From 93b610cc7255eb0a1c09408554a70bf66b6c5f38 Mon Sep 17 00:00:00 2001 From: Vinnie Falco Date: Wed, 14 Jan 2026 11:44:28 -0800 Subject: [PATCH 1/5] refactor: thread_pool now inherits from execution_context with executor_type - Move thread_pool to ex/ namespace (ex/thread_pool.hpp, ex/thread_pool.cpp) - Derive thread_pool from execution_context for service management - Add nested executor_type class satisfying capy::executor concept - Add coro_handler to wrap any_coro for pool execution - Add work counting with atomic counter in impl - Add std::optional> member to async_run promise_type - Disable frame_allocator operator new/delete overloads (VFALCO turned off) - Expand thread_pool tests: executor concept, equality, services, concurrent post - Update documentation with executor operations and service examples --- include/boost/capy/ex/async_run.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/include/boost/capy/ex/async_run.hpp b/include/boost/capy/ex/async_run.hpp index 33f2b24..6eb3f6c 100644 --- a/include/boost/capy/ex/async_run.hpp +++ b/include/boost/capy/ex/async_run.hpp @@ -104,6 +104,7 @@ struct async_run_task Dispatcher d_; Handler handler_; std::exception_ptr ep_; + std::optional> t_; template promise_type(D&& d, H&& h, Args&&...) From 5d331643727ba76ee249fd5bda69455d74fb5951 Mon Sep 17 00:00:00 2001 From: Vinnie Falco Date: Wed, 14 Jan 2026 20:23:48 -0800 Subject: [PATCH 2/5] refactor: simplify thread_pool by removing custom allocator - Remove work_allocator arena-based memory pool (work_allocator.cpp/hpp) - Replace manual linked list with execution_context::queue - Switch to standard new/delete for handler allocation - Inline empty on_work_started/on_work_finished in header - Update example in documentation to use async_run --- include/boost/capy/ex/thread_pool.hpp | 14 +- src/ex/thread_pool.cpp | 134 ++----------- src/work_allocator.cpp | 268 -------------------------- src/work_allocator.hpp | 177 ----------------- 4 files changed, 23 insertions(+), 570 deletions(-) delete mode 100644 src/work_allocator.cpp delete mode 100644 src/work_allocator.hpp diff --git a/include/boost/capy/ex/thread_pool.hpp b/include/boost/capy/ex/thread_pool.hpp index 61972cc..768eb4c 100644 --- a/include/boost/capy/ex/thread_pool.hpp +++ b/include/boost/capy/ex/thread_pool.hpp @@ -18,7 +18,7 @@ namespace boost { namespace capy { -/** A thread pool execution context for running work asynchronously. +/** A pool of threads for running work asynchronously. This class provides a pool of worker threads that execute submitted work items. It inherits from `execution_context`, @@ -38,7 +38,7 @@ namespace capy { auto ex = pool.get_executor(); // Post a coroutine for execution - ex.post(my_coroutine_handle); + async_run(ex)( my_coro() ); @endcode @see execution_context, executor @@ -131,18 +131,20 @@ class thread_pool::executor_type Must be paired with `on_work_finished()`. */ - BOOST_CAPY_DECL void - on_work_started() const noexcept; + on_work_started() const noexcept + { + } /** Informs the executor that work has completed. @par Preconditions A preceding call to `on_work_started()` on an equal executor. */ - BOOST_CAPY_DECL void - on_work_finished() const noexcept; + on_work_finished() const noexcept + { + } /** Dispatch a coroutine handle. diff --git a/src/ex/thread_pool.cpp b/src/ex/thread_pool.cpp index 498868b..8fd4712 100644 --- a/src/ex/thread_pool.cpp +++ b/src/ex/thread_pool.cpp @@ -7,10 +7,7 @@ // Official repository: https://github.com/boostorg/capy // -#include "src/work_allocator.hpp" - #include -#include #include #include #include @@ -34,12 +31,14 @@ class coro_handler : public execution_context::handler void operator()() override { - h_.resume(); + auto h = h_; + delete this; + h.resume(); } void destroy() override { - // Coroutine handle is not owned, nothing to destroy + delete this; } }; @@ -47,35 +46,12 @@ class coro_handler : public execution_context::handler class thread_pool::impl { - // Prepended to each work allocation to track metadata - struct header - { - header* next; - std::size_t size; - std::size_t align; - }; - std::mutex mutex_; std::condition_variable cv_; - header* head_; - header* tail_; + execution_context::queue q_; std::vector threads_; - work_allocator arena_; - std::atomic work_count_; bool stop_; - static header* - to_header(void* p) noexcept - { - return static_cast(p) - 1; - } - - static void* - from_header(header* h) noexcept - { - return h + 1; - } - public: ~impl() { @@ -88,23 +64,12 @@ class thread_pool::impl for(auto& t : threads_) t.join(); - // Drain remaining work (no lock needed, threads are joined) - while(head_) - { - header* h = head_; - head_ = head_->next; - auto* w = static_cast(from_header(h)); - w->destroy(); - arena_.deallocate(h, h->size, h->align); - } + // Remaining handlers destroyed by queue destructor } explicit impl(std::size_t num_threads) - : head_(nullptr) - , tail_(nullptr) - , work_count_(0) - , stop_(false) + : stop_(false) { if(num_threads == 0) num_threads = std::thread::hardware_concurrency(); @@ -117,91 +82,36 @@ class thread_pool::impl } void - on_work_started() noexcept - { - ++work_count_; - } - - void - on_work_finished() noexcept - { - --work_count_; - } - - void* - allocate(std::size_t size, std::size_t align) - { - // Allocate space for header + work object - std::size_t total = sizeof(header) + size; - std::lock_guard lock(mutex_); - void* p = arena_.allocate(total, align); - auto* h = new(p) header{nullptr, total, align}; - return from_header(h); - } - - void - deallocate(void* p, std::size_t, std::size_t) noexcept - { - // Size/align from caller are ignored; we use stored values - header* h = to_header(p); - std::lock_guard lock(mutex_); - arena_.deallocate(h, h->size, h->align); - } - - void - submit(execution_context::handler* h) + post(any_coro h) { - header* hdr = to_header(h); + auto* handler = new coro_handler(h); { std::lock_guard lock(mutex_); - hdr->next = nullptr; - if(tail_) - tail_->next = hdr; - else - head_ = hdr; - tail_ = hdr; + q_.push(handler); } cv_.notify_one(); } - void - post(any_coro h) - { - // Allocate handler and submit - void* p = allocate(sizeof(coro_handler), alignof(coro_handler)); - auto* handler = new(p) coro_handler(h); - submit(handler); - } - private: void run() { for(;;) { - header* h = nullptr; + execution_context::handler* h = nullptr; { std::unique_lock lock(mutex_); cv_.wait(lock, [this]{ - return stop_ || head_ != nullptr; + return stop_ || !q_.empty(); }); - if(stop_ && !head_) + if(stop_ && q_.empty()) return; - h = head_; - head_ = head_->next; - if(!head_) - tail_ = nullptr; + h = q_.pop(); } - auto* w = static_cast(from_header(h)); - (*w)(); - - { - std::lock_guard lock(mutex_); - arena_.deallocate(h, h->size, h->align); - } + (*h)(); } } }; @@ -224,20 +134,6 @@ thread_pool(std::size_t num_threads) //------------------------------------------------------------------------------ -void -thread_pool::executor_type:: -on_work_started() const noexcept -{ - pool_->impl_->on_work_started(); -} - -void -thread_pool::executor_type:: -on_work_finished() const noexcept -{ - pool_->impl_->on_work_finished(); -} - void thread_pool::executor_type:: post(any_coro h) const diff --git a/src/work_allocator.cpp b/src/work_allocator.cpp deleted file mode 100644 index f415788..0000000 --- a/src/work_allocator.cpp +++ /dev/null @@ -1,268 +0,0 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/boostorg/capy -// - -#include "work_allocator.hpp" -#include -#include -#include - -namespace boost { -namespace capy { - -//------------------------------------------------------------------------------ -// -// work_allocator::arena -// -//------------------------------------------------------------------------------ - -work_allocator::arena:: -~arena() -{ - std::free(base_); -} - -work_allocator::arena:: -arena(std::size_t capacity) - : prev_(nullptr) - , next_(nullptr) - , base_(std::malloc(capacity)) - , capacity_(capacity) - , offset_(capacity) - , count_(0) -{ - if(!base_) - throw std::bad_alloc(); -} - -bool -work_allocator::arena:: -owns(void* p) const noexcept -{ - std::less cmp; - auto* cp = static_cast(p); - auto* base = static_cast(base_); - // cp >= base && cp < base + capacity_ - return !cmp(cp, base) && cmp(cp, base + capacity_); -} - -void* -work_allocator::arena:: -allocate(std::size_t size, std::size_t align) noexcept -{ - if(offset_ < size) - return nullptr; - - std::size_t aligned = (offset_ - size) & ~(align - 1); - - if(aligned > offset_) - return nullptr; - - offset_ = aligned; - ++count_; - return static_cast(base_) + offset_; -} - -void -work_allocator::arena:: -deallocate(void* /*p*/, std::size_t /*size*/, std::size_t /*align*/) noexcept -{ - --count_; -} - -void -work_allocator::arena:: -reset() noexcept -{ - offset_ = capacity_; - count_ = 0; -} - -//------------------------------------------------------------------------------ -// -// work_allocator -// -//------------------------------------------------------------------------------ - -work_allocator:: -~work_allocator() -{ - arena* a = head_; - while(a) - { - arena* next = a->next_; - delete a; - a = next; - } -} - -work_allocator:: -work_allocator( - std::size_t min_size, - std::size_t max_size, - std::size_t keep_empty) - : head_(nullptr) - , tail_(nullptr) - , arena_count_(0) - , next_size_(min_size) - , min_size_(min_size) - , max_size_(max_size) - , keep_empty_(keep_empty) -{ -} - -void* -work_allocator:: -allocate(std::size_t size, std::size_t align) -{ - // Always allocate from tail (active arena) - if(tail_) - { - if(void* p = tail_->allocate(size, align)) - return p; - } - - // Active arena full or none exists. - // Try recycling a parked arena to preserve allocation order. - arena* fresh = find_parked(); - if(fresh) - { - unlink(fresh); - fresh->reset(); - link_at_tail(fresh); - } - else - { - std::size_t arena_size = next_size_; - if(arena_size < size + align) - arena_size = size + align; - - fresh = new arena(arena_size); - link_at_tail(fresh); - - if(next_size_ < max_size_) - { - next_size_ *= 2; - if(next_size_ > max_size_) - next_size_ = max_size_; - } - } - - void* p = tail_->allocate(size, align); - if(!p) - throw std::bad_alloc(); - return p; -} - -void -work_allocator:: -deallocate(void* p, std::size_t size, std::size_t align) noexcept -{ - arena* a = find_arena(p); - if(!a) - return; - - a->deallocate(p, size, align); - - // Prune when a non-active arena empties - if(a->empty() && a != tail_) - prune(); -} - -void -work_allocator:: -link_at_tail(arena* a) noexcept -{ - a->prev_ = tail_; - a->next_ = nullptr; - if(tail_) - tail_->next_ = a; - else - head_ = a; - tail_ = a; - ++arena_count_; -} - -void -work_allocator:: -unlink(arena* a) noexcept -{ - if(a->prev_) - a->prev_->next_ = a->next_; - else - head_ = a->next_; - - if(a->next_) - a->next_->prev_ = a->prev_; - else - tail_ = a->prev_; - - a->prev_ = nullptr; - a->next_ = nullptr; - --arena_count_; -} - -work_allocator::arena* -work_allocator:: -find_arena(void* p) noexcept -{ - for(arena* a = head_; a; a = a->next_) - { - if(a->owns(p)) - return a; - } - return nullptr; -} - -work_allocator::arena* -work_allocator:: -find_parked() noexcept -{ - // Search from oldest, skip the active arena (tail) - for(arena* a = head_; a && a != tail_; a = a->next_) - { - if(a->empty()) - return a; - } - return nullptr; -} - -void -work_allocator:: -prune() noexcept -{ - // Count parked (empty non-active) arenas - std::size_t parked_count = 0; - for(arena* a = head_; a && a != tail_; a = a->next_) - { - if(a->empty()) - ++parked_count; - } - - // Delete excess parked arenas from the front - arena* a = head_; - while(a && a != tail_ && parked_count > keep_empty_) - { - arena* next = a->next_; - if(a->empty()) - { - unlink(a); - delete a; - --parked_count; - } - a = next; - } - - // Shrink next_size if we're back to minimal state - if(arena_count_ <= 1) - next_size_ = min_size_; -} - -} // capy -} // boost - diff --git a/src/work_allocator.hpp b/src/work_allocator.hpp deleted file mode 100644 index c0e8406..0000000 --- a/src/work_allocator.hpp +++ /dev/null @@ -1,177 +0,0 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/boostorg/capy -// - -#ifndef BOOST_CAPY_SRC_WORK_ALLOCATOR_HPP -#define BOOST_CAPY_SRC_WORK_ALLOCATOR_HPP - -#include - -namespace boost { -namespace capy { - -/** A pool of arenas for dynamic allocation patterns. - - @par Allocation Order Invariant - - Allocations always come from the newest arena (tail). - Once an arena is superseded by a newer one, it never - receives new allocations. This ensures all allocations - in arena N occurred before all allocations in arena N+1. - - Deallocations can occur in any order. When an older - arena empties, it becomes "parked" — held for recycling - rather than deleted immediately. - - @par Arena States - - - Active: The tail arena; receives all new allocations. - - Draining: Older arenas with outstanding allocations. - - Parked: Empty arenas awaiting recycling or deletion. - - @par Recycling - - When the active arena fills, a parked arena may be - recycled as the new active arena. This avoids malloc/free - churn under steady-state load. Recycled arenas are moved - to the tail of the list, becoming the new active arena. - - This class is not thread-safe. -*/ -class work_allocator -{ -public: - class arena; - -private: - arena* head_; - arena* tail_; - std::size_t arena_count_; - std::size_t next_size_; - std::size_t min_size_; - std::size_t max_size_; - std::size_t keep_empty_; - -public: - ~work_allocator(); - - explicit - work_allocator( - std::size_t min_size = 4096, - std::size_t max_size = 1048576, - std::size_t keep_empty = 1); - - work_allocator(work_allocator const&) = delete; - work_allocator& operator=(work_allocator const&) = delete; - - /** Return the number of arenas. - */ - std::size_t - arena_count() const noexcept - { - return arena_count_; - } - - /** Return allocated memory. - - @throws std::bad_alloc on failure. - */ - void* allocate(std::size_t size, std::size_t align); - - /** Release an allocation. - */ - void deallocate(void* p, std::size_t size, std::size_t align) noexcept; - -private: - void link_at_tail(arena* a) noexcept; - void unlink(arena* a) noexcept; - arena* find_arena(void* p) noexcept; - arena* find_parked() noexcept; - void prune() noexcept; -}; - -//------------------------------------------------------------------------------ - -/** A fixed-size arena that allocates from high to low addresses. - - Memory is allocated from the top of the buffer downward. - Deallocation only decrements a counter; actual memory is - reused only when all allocations are released. - - Arenas are linked in a doubly-linked list managed by - work_allocator. -*/ -class work_allocator::arena -{ - friend class work_allocator; - - arena* prev_; - arena* next_; - void* base_; - std::size_t capacity_; - std::size_t offset_; - std::size_t count_; - -public: - ~arena(); - - explicit - arena(std::size_t capacity); - - arena(arena const&) = delete; - arena& operator=(arena const&) = delete; - - /** Return the total capacity in bytes. - */ - std::size_t - capacity() const noexcept - { - return capacity_; - } - - /** Return the number of active allocations. - */ - std::size_t - count() const noexcept - { - return count_; - } - - /** Return true if there are no active allocations. - */ - bool - empty() const noexcept - { - return count_ == 0; - } - - /** Return true if the pointer is within this arena. - */ - bool - owns(void* p) const noexcept; - - /** Return allocated memory, or nullptr if full. - */ - void* - allocate(std::size_t size, std::size_t align) noexcept; - - /** Release an allocation. - */ - void - deallocate(void* p, std::size_t size, std::size_t align) noexcept; - - /** Reset the arena for reuse. - */ - void - reset() noexcept; -}; - -} // capy -} // boost - -#endif From 659e2f3d24b3d63010bdba560c289d83228d1cb9 Mon Sep 17 00:00:00 2001 From: Vinnie Falco Date: Wed, 14 Jan 2026 21:56:33 -0800 Subject: [PATCH 3/5] refactor: remove handler and queue from execution_context Move handler abstraction out of the public API. The thread_pool now uses a local work struct internally, and handler/queue tests are removed since the abstraction is no longer part of execution_context. --- include/boost/capy/ex/execution_context.hpp | 202 -------------------- src/ex/thread_pool.cpp | 56 +++--- test/unit/ex/execution_context.cpp | 38 ---- test/unit/executor.cpp | 148 -------------- 4 files changed, 27 insertions(+), 417 deletions(-) diff --git a/include/boost/capy/ex/execution_context.hpp b/include/boost/capy/ex/execution_context.hpp index 223d034..7cf6956 100644 --- a/include/boost/capy/ex/execution_context.hpp +++ b/include/boost/capy/ex/execution_context.hpp @@ -12,7 +12,6 @@ #include #include -#include #include #include #include @@ -159,207 +158,6 @@ class BOOST_CAPY_DECL //------------------------------------------------ - /** Abstract base class for completion handlers. - - Handlers are continuations that execute after an asynchronous - operation completes. They can be queued for deferred invocation, - allowing callbacks and coroutine resumptions to be posted to an - executor. - - Handlers should execute quickly - typically just initiating - another I/O operation or suspending on a foreign task. Heavy - computation should be avoided in handlers to prevent blocking - the event loop. - - Handlers may be heap-allocated or may be data members of an - enclosing object. The allocation strategy is determined by the - creator of the handler. - - @par Ownership Contract - - Callers must invoke exactly ONE of `operator()` or `destroy()`, - never both: - - @li `operator()` - Invokes the handler. The handler is - responsible for its own cleanup (typically `delete this` - for heap-allocated handlers). The caller must not call - `destroy()` after invoking this. - - @li `destroy()` - Destroys an uninvoked handler. This is - called when a queued handler must be discarded without - execution, such as during shutdown or exception cleanup. - For heap-allocated handlers, this typically calls - `delete this`. - - @par Exception Safety - - Implementations of `operator()` must perform cleanup before - any operation that might throw. This ensures that if the handler - throws, the exception propagates cleanly to the caller of - `run()` without leaking resources. Typical pattern: - - @code - void operator()() override - { - auto h = h_; - delete this; // cleanup FIRST - h.resume(); // then resume (may throw) - } - @endcode - - This "delete-before-invoke" pattern also enables memory - recycling - the handler's memory can be reused immediately - by subsequent allocations. - - @note Callers must never delete handlers directly with `delete`; - use `operator()` for normal invocation or `destroy()` for cleanup. - - @note Heap-allocated handlers are typically allocated with - custom allocators to minimize allocation overhead in - high-frequency async operations. - - @note Some handlers (such as those owned by containers like - `std::unique_ptr` or embedded as data members) are not meant to - be destroyed and should implement both functions as no-ops - (for `operator()`, invoke the continuation but don't delete). - - @see queue - */ - class handler : public intrusive_queue::node - { - public: - virtual void operator()() = 0; - virtual void destroy() = 0; - - /** Returns the user-defined data pointer. - - Derived classes may set this to store auxiliary data - such as a pointer to the most-derived object. - - @par Postconditions - @li Initially returns `nullptr` for newly constructed handlers. - @li Returns the current value of `data_` if modified by a derived class. - - @return The user-defined data pointer, or `nullptr` if not set. - */ - void* data() const noexcept - { - return data_; - } - - protected: - ~handler() = default; - - void* data_ = nullptr; - }; - - //------------------------------------------------ - - /** An intrusive FIFO queue of handlers. - - This queue stores handlers using an intrusive linked list, - avoiding additional allocations for queue nodes. Handlers - are popped in the order they were pushed (first-in, first-out). - - The destructor calls `destroy()` on any remaining handlers. - - @note This is not thread-safe. External synchronization is - required for concurrent access. - - @see handler - */ - class queue - { - intrusive_queue q_; - - public: - /** Default constructor. - - Creates an empty queue. - - @post `empty() == true` - */ - queue() = default; - - /** Move constructor. - - Takes ownership of all handlers from `other`, - leaving `other` empty. - - @param other The queue to move from. - - @post `other.empty() == true` - */ - queue(queue&& other) noexcept - : q_(std::move(other.q_)) - { - } - - queue(queue const&) = delete; - queue& operator=(queue const&) = delete; - queue& operator=(queue&&) = delete; - - /** Destructor. - - Calls `destroy()` on any remaining handlers in the queue. - */ - ~queue() - { - while(auto* h = q_.pop()) - h->destroy(); - } - - /** Return true if the queue is empty. - - @return `true` if the queue contains no handlers. - */ - bool - empty() const noexcept - { - return q_.empty(); - } - - /** Add a handler to the back of the queue. - - @param h Pointer to the handler to add. - - @pre `h` is not null and not already in a queue. - */ - void - push(handler* h) noexcept - { - q_.push(h); - } - - /** Splice all handlers from another queue to the back. - - All handlers from `other` are moved to the back of this - queue. After this call, `other` is empty. - - @param other The queue to splice from. - - @post `other.empty() == true` - */ - void - push(queue& other) noexcept - { - q_.splice(other.q_); - } - - /** Remove and return the front handler. - - @return Pointer to the front handler, or `nullptr` - if the queue is empty. - */ - handler* - pop() noexcept - { - return q_.pop(); - } - }; - - //------------------------------------------------ - execution_context(execution_context const&) = delete; execution_context& operator=(execution_context const&) = delete; diff --git a/src/ex/thread_pool.cpp b/src/ex/thread_pool.cpp index 8fd4712..4c2b612 100644 --- a/src/ex/thread_pool.cpp +++ b/src/ex/thread_pool.cpp @@ -8,6 +8,7 @@ // #include +#include #include #include #include @@ -18,37 +19,33 @@ namespace capy { //------------------------------------------------------------------------------ -// Handler that wraps an any_coro for execution -class coro_handler : public execution_context::handler +class thread_pool::impl { - any_coro h_; - -public: - explicit coro_handler(any_coro h) noexcept - : h_(h) + struct work : intrusive_queue::node { - } + any_coro h_; - void operator()() override - { - auto h = h_; - delete this; - h.resume(); - } + explicit work(any_coro h) noexcept + : h_(h) + { + } - void destroy() override - { - delete this; - } -}; + void run() + { + auto h = h_; + delete this; + h.resume(); + } -//------------------------------------------------------------------------------ + void destroy() + { + delete this; + } + }; -class thread_pool::impl -{ std::mutex mutex_; std::condition_variable cv_; - execution_context::queue q_; + intrusive_queue q_; std::vector threads_; bool stop_; @@ -64,7 +61,8 @@ class thread_pool::impl for(auto& t : threads_) t.join(); - // Remaining handlers destroyed by queue destructor + while(auto* w = q_.pop()) + w->destroy(); } explicit @@ -84,10 +82,10 @@ class thread_pool::impl void post(any_coro h) { - auto* handler = new coro_handler(h); + auto* w = new work(h); { std::lock_guard lock(mutex_); - q_.push(handler); + q_.push(w); } cv_.notify_one(); } @@ -98,7 +96,7 @@ class thread_pool::impl { for(;;) { - execution_context::handler* h = nullptr; + work* w = nullptr; { std::unique_lock lock(mutex_); cv_.wait(lock, [this]{ @@ -108,10 +106,10 @@ class thread_pool::impl if(stop_ && q_.empty()) return; - h = q_.pop(); + w = q_.pop(); } - (*h)(); + w->run(); } } }; diff --git a/test/unit/ex/execution_context.cpp b/test/unit/ex/execution_context.cpp index c49fefb..79b7251 100644 --- a/test/unit/ex/execution_context.cpp +++ b/test/unit/ex/execution_context.cpp @@ -128,16 +128,6 @@ struct nested_service : execution_context::service void shutdown() override {} }; -// Concrete handler for testing data() behavior -struct test_handler : execution_context::handler -{ - void operator()() override {} - void destroy() override {} - - // Expose data_ for testing - void set_data(void* p) { data_ = p; } -}; - } // namespace struct execution_context_test @@ -333,32 +323,6 @@ struct execution_context_test BOOST_TEST(ctx.has_service()); } - void - testHandlerDataInitiallyNull() - { - test_handler h; - BOOST_TEST_EQ(h.data(), nullptr); - } - - void - testHandlerDataReflectsChanges() - { - test_handler h; - int dummy = 42; - - h.set_data(&dummy); - BOOST_TEST_EQ(h.data(), &dummy); - - // Change to different value - double other = 3.14; - h.set_data(&other); - BOOST_TEST_EQ(h.data(), &other); - - // Change back to nullptr - h.set_data(nullptr); - BOOST_TEST_EQ(h.data(), nullptr); - } - void run() { @@ -374,8 +338,6 @@ struct execution_context_test testMultipleServices(); testNestedServiceCreation(); testConcurrentAccess(); - testHandlerDataInitiallyNull(); - testHandlerDataReflectsChanges(); } }; diff --git a/test/unit/executor.cpp b/test/unit/executor.cpp index 3214301..bb998da 100644 --- a/test/unit/executor.cpp +++ b/test/unit/executor.cpp @@ -18,29 +18,6 @@ namespace boost { namespace capy { -// Test handler implementation -struct test_handler : execution_context::handler -{ - int& invoked; - int& destroyed; - - test_handler(int& i, int& d) - : invoked(i) - , destroyed(d) - { - } - - void operator()() override - { - ++invoked; - } - - void destroy() override - { - ++destroyed; - } -}; - // Minimal execution context for testing struct test_context { @@ -111,131 +88,6 @@ struct executor_test void run() { - // handler - invoke operator() - { - int invoked = 0; - int destroyed = 0; - test_handler h(invoked, destroyed); - - h(); - - BOOST_TEST(invoked == 1); - BOOST_TEST(destroyed == 0); - } - - // handler - invoke destroy() - { - int invoked = 0; - int destroyed = 0; - test_handler h(invoked, destroyed); - - h.destroy(); - - BOOST_TEST(invoked == 0); - BOOST_TEST(destroyed == 1); - } - - // queue - default construction - { - execution_context::queue q; - BOOST_TEST(q.empty()); - BOOST_TEST(q.pop() == nullptr); - } - - // queue - push and pop - { - int invoked = 0; - int destroyed = 0; - test_handler h(invoked, destroyed); - - execution_context::queue q; - q.push(&h); - BOOST_TEST(!q.empty()); - - execution_context::handler* p = q.pop(); - BOOST_TEST(p == &h); - BOOST_TEST(q.empty()); - } - - // queue - FIFO order - { - int invoked1 = 0, destroyed1 = 0; - int invoked2 = 0, destroyed2 = 0; - int invoked3 = 0, destroyed3 = 0; - test_handler h1(invoked1, destroyed1); - test_handler h2(invoked2, destroyed2); - test_handler h3(invoked3, destroyed3); - - execution_context::queue q; - q.push(&h1); - q.push(&h2); - q.push(&h3); - - BOOST_TEST(q.pop() == &h1); - BOOST_TEST(q.pop() == &h2); - BOOST_TEST(q.pop() == &h3); - BOOST_TEST(q.empty()); - } - - // queue - move constructor - { - int invoked = 0; - int destroyed = 0; - test_handler h(invoked, destroyed); - - execution_context::queue q1; - q1.push(&h); - - execution_context::queue q2(std::move(q1)); - BOOST_TEST(q1.empty()); - BOOST_TEST(!q2.empty()); - BOOST_TEST(q2.pop() == &h); - } - - // queue - splice - { - int i1 = 0, d1 = 0; - int i2 = 0, d2 = 0; - int i3 = 0, d3 = 0; - int i4 = 0, d4 = 0; - test_handler h1(i1, d1); - test_handler h2(i2, d2); - test_handler h3(i3, d3); - test_handler h4(i4, d4); - - execution_context::queue q1; - execution_context::queue q2; - q1.push(&h1); - q1.push(&h2); - q2.push(&h3); - q2.push(&h4); - - q1.push(q2); - - BOOST_TEST(q2.empty()); - BOOST_TEST(q1.pop() == &h1); - BOOST_TEST(q1.pop() == &h2); - BOOST_TEST(q1.pop() == &h3); - BOOST_TEST(q1.pop() == &h4); - BOOST_TEST(q1.empty()); - } - - // queue - destructor calls destroy - { - int invoked = 0; - int destroyed = 0; - test_handler h(invoked, destroyed); - - { - execution_context::queue q; - q.push(&h); - // destructor should call destroy() - } - - BOOST_TEST(invoked == 0); - BOOST_TEST(destroyed == 1); - } - // executor - equality comparison { test_context ctx1; From 3b24f7e8f770d6427aa002508985df80ca5cf046 Mon Sep 17 00:00:00 2001 From: Vinnie Falco Date: Wed, 14 Jan 2026 22:17:06 -0800 Subject: [PATCH 4/5] docs: update thread_pool Javadoc and add implementation comments - Simplify class documentation with clearer problem statements - Add thread safety notes for thread_pool and executor_type - Convert verbose member docs to brief style where appropriate - Add implementation comments explaining key design decisions --- .gitignore | 2 +- include/boost/capy/concept/executor.hpp | 28 ++++--- include/boost/capy/ex/thread_pool.hpp | 102 +++++++++--------------- src/ex/thread_pool.cpp | 11 ++- 4 files changed, 67 insertions(+), 76 deletions(-) diff --git a/.gitignore b/.gitignore index 9aed4ec..a75e0d6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ /.vscode/ -/build/ +/build/* !/build/Jamfile !/build/brotli.jam /out/ diff --git a/include/boost/capy/concept/executor.hpp b/include/boost/capy/concept/executor.hpp index 1d25f48..5c30c5c 100644 --- a/include/boost/capy/concept/executor.hpp +++ b/include/boost/capy/concept/executor.hpp @@ -38,15 +38,25 @@ namespace capy { `on_work_started()` on an equal executor. @li `dispatch(h)` - Execute a coroutine, potentially immediately - if the executor determines it is safe to do so. + if the executor determines it is safe to do so. The executor + may block forward progress of the caller until execution + completes. - @li `post(h)` - Queue a coroutine for later execution. Shall not - block forward progress of the caller. + @li `post(h)` - Queue a coroutine for later execution. The + executor shall not block forward progress of the caller + pending completion. - @li `defer(h)` - Queue a coroutine for later execution, with - a hint that the caller prefers deferral. Semantically - identical to `post`, but conveys that the coroutine is a - continuation of the current call context. + @li `defer(h)` - Queue a coroutine for later execution. The + executor shall not block forward progress of the caller + pending completion. Semantically identical to `post`, but + conveys a preference that the coroutine is a continuation + of the current call context. The executor may use this + information to optimize or otherwise adjust invocation. + + @par Synchronization + + The invocation of `dispatch`, `post`, or `defer` synchronizes + with the invocation of the coroutine. @par No-Throw Guarantee @@ -78,8 +88,8 @@ concept executor = std::copy_constructible && std::equality_comparable && requires(E& e, E const& ce, std::coroutine_handle<> h) { - // Execution context access - { ce.context() } -> std::same_as; + // Execution context access (must not throw) + { ce.context() } noexcept -> std::same_as; // Work tracking (must not throw) { ce.on_work_started() } noexcept; diff --git a/include/boost/capy/ex/thread_pool.hpp b/include/boost/capy/ex/thread_pool.hpp index 768eb4c..a31f1a2 100644 --- a/include/boost/capy/ex/thread_pool.hpp +++ b/include/boost/capy/ex/thread_pool.hpp @@ -18,30 +18,24 @@ namespace boost { namespace capy { -/** A pool of threads for running work asynchronously. +/** A pool of threads for executing work concurrently. - This class provides a pool of worker threads that execute - submitted work items. It inherits from `execution_context`, - providing service management and a nested `executor_type` - that satisfies the `capy::executor` concept. - - Work is submitted via the executor obtained from `get_executor()`. - The executor's `post()`, `dispatch()`, and `defer()` functions - queue coroutines for execution on pool threads. + Use this when you need to run coroutines on multiple threads + without the overhead of creating and destroying threads for + each task. Work items are distributed across the pool using + a shared queue. @par Thread Safety - All member functions may be called concurrently. + Distinct objects: Safe. + Shared objects: Unsafe. @par Example @code thread_pool pool(4); // 4 worker threads auto ex = pool.get_executor(); - - // Post a coroutine for execution - async_run(ex)( my_coro() ); + ex.post(some_coroutine); + // pool destructor waits for all work to complete @endcode - - @see execution_context, executor */ class BOOST_CAPY_DECL thread_pool @@ -53,17 +47,21 @@ class BOOST_CAPY_DECL public: class executor_type; - /** Destructor. + /** Destroy the thread pool. - Signals all threads to stop and waits for them to complete. - Calls `shutdown()` and `destroy()` to clean up services. + Signals all worker threads to stop, waits for them to + finish, and destroys any pending work items. */ ~thread_pool(); /** Construct a thread pool. - @param num_threads The number of worker threads to create. - If zero, defaults to the hardware concurrency. + Creates a pool with the specified number of worker threads. + If `num_threads` is zero, the number of threads is set to + the hardware concurrency, or one if that cannot be determined. + + @param num_threads The number of worker threads, or zero + for automatic selection. */ explicit thread_pool(std::size_t num_threads = 0); @@ -73,9 +71,6 @@ class BOOST_CAPY_DECL /** Return an executor for this thread pool. - The returned executor can be used to post work items - to this thread pool. - @return An executor associated with this thread pool. */ executor_type @@ -84,18 +79,13 @@ class BOOST_CAPY_DECL //------------------------------------------------------------------------------ -/** An executor for dispatching work to a thread_pool. - - The executor provides the interface for posting work items - to the associated thread_pool. It satisfies the `capy::executor` - concept. +/** An executor that submits work to a thread_pool. - Executors are lightweight handles that can be copied and compared - for equality. Two executors compare equal if they refer to the - same thread_pool. + Executors are lightweight handles that can be copied and stored. + All copies refer to the same underlying thread pool. @par Thread Safety - Distinct objects: Safe.@n + Distinct objects: Safe. Shared objects: Safe. */ class thread_pool::executor_type @@ -111,49 +101,37 @@ class thread_pool::executor_type } public: - /** Default constructor. - - Constructs an executor not associated with any thread_pool. - */ + /// Default construct a null executor. executor_type() = default; - /** Return a reference to the associated execution context. - - @return Reference to the thread_pool. - */ + /// Return the underlying thread pool. thread_pool& context() const noexcept { return *pool_; } - /** Informs the executor that work is beginning. - - Must be paired with `on_work_finished()`. - */ + /// Notify that work has started (no-op for thread pools). void on_work_started() const noexcept { } - /** Informs the executor that work has completed. - - @par Preconditions - A preceding call to `on_work_started()` on an equal executor. - */ + /// Notify that work has finished (no-op for thread pools). void on_work_finished() const noexcept { } - /** Dispatch a coroutine handle. + /** Submit a coroutine for execution. - For thread_pool, dispatch always posts the work since - the calling thread is never "inside" the pool's run loop. + Posts the coroutine to the thread pool and returns + immediately. The caller should suspend after calling + this function. - @param h The coroutine handle to dispatch. + @param h The coroutine handle to execute. - @return `std::noop_coroutine()` since work is always posted. + @return A noop coroutine handle to resume. */ any_coro dispatch(any_coro h) const @@ -162,23 +140,22 @@ class thread_pool::executor_type return std::noop_coroutine(); } - /** Post a coroutine for deferred execution. + /** Post a coroutine to the thread pool. The coroutine will be resumed on one of the pool's worker threads. - @param h The coroutine handle to post. + @param h The coroutine handle to execute. */ BOOST_CAPY_DECL void post(any_coro h) const; - /** Queue a coroutine for deferred execution. + /** Defer a coroutine to the thread pool. - This is semantically identical to `post`, but conveys that - `h` is a continuation of the current call context. + Equivalent to post() for thread pools. - @param h The coroutine handle to defer. + @param h The coroutine handle to execute. */ void defer(any_coro h) const @@ -186,10 +163,7 @@ class thread_pool::executor_type post(h); } - /** Compare two executors for equality. - - @return `true` if both executors refer to the same thread_pool. - */ + /// Return true if two executors refer to the same thread pool. bool operator==(executor_type const& other) const noexcept { diff --git a/src/ex/thread_pool.cpp b/src/ex/thread_pool.cpp index 4c2b612..a9957f4 100644 --- a/src/ex/thread_pool.cpp +++ b/src/ex/thread_pool.cpp @@ -19,8 +19,10 @@ namespace capy { //------------------------------------------------------------------------------ +// Pimpl implementation hides threading details from the header class thread_pool::impl { + // Wraps a coroutine handle for queue storage struct work : intrusive_queue::node { any_coro h_; @@ -32,6 +34,7 @@ class thread_pool::impl void run() { + // delete before dispatch auto h = h_; delete this; h.resume(); @@ -61,6 +64,7 @@ class thread_pool::impl for(auto& t : threads_) t.join(); + // Destroy any work items that were never executed while(auto* w = q_.pop()) w->destroy(); } @@ -69,9 +73,10 @@ class thread_pool::impl impl(std::size_t num_threads) : stop_(false) { - if(num_threads == 0) + if( num_threads == 0) num_threads = std::thread::hardware_concurrency(); - if(num_threads == 0) + // Fallback + if( num_threads == 0) num_threads = 1; threads_.reserve(num_threads); @@ -103,6 +108,7 @@ class thread_pool::impl return stop_ || !q_.empty(); }); + // Only exit when stopped AND queue is drained if(stop_ && q_.empty()) return; @@ -119,6 +125,7 @@ class thread_pool::impl thread_pool:: ~thread_pool() { + // Order matters: shutdown services, then impl, then base shutdown(); delete impl_; destroy(); From 0888e4305cc9181563efe75132e72d49ee7e9525 Mon Sep 17 00:00:00 2001 From: Steve Gerbino Date: Thu, 15 Jan 2026 21:14:19 +0100 Subject: [PATCH 5/5] Simplify the build matrix --- .drone.star => .drone.star.disabled | 0 .github/workflows/ci.yml | 490 ++++------------------------ .gitignore | 1 + 3 files changed, 63 insertions(+), 428 deletions(-) rename .drone.star => .drone.star.disabled (100%) diff --git a/.drone.star b/.drone.star.disabled similarity index 100% rename from .drone.star rename to .drone.star.disabled diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0d6f168..27aa7df 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,18 +38,23 @@ env: TZ: "Europe/London" jobs: - runner-selection: - name: Runner Selection - runs-on: ${{ github.repository_owner == 'boostorg' && fromJSON('[ "self-hosted", "linux", "x64", "ubuntu-latest-aws" ]') || 'ubuntu-latest' }} - outputs: - labelmatrix: ${{ steps.aws_hosted_runners.outputs.labelmatrix }} - steps: - - name: AWS Hosted Runners - id: aws_hosted_runners - uses: cppalliance/aws-hosted-runners@v1.0.0 + # Self-hosted runner selection is disabled to allow re-running individual + # failed jobs from the GitHub Actions UI. When using dynamic runner selection, + # the runs-on value depends on this job's output, which isn't available when + # re-running a subset of jobs. + # + # runner-selection: + # name: Runner Selection + # runs-on: ${{ github.repository_owner == 'boostorg' && fromJSON('[ "self-hosted", "linux", "x64", "ubuntu-latest-aws" ]') || 'ubuntu-latest' }} + # outputs: + # labelmatrix: ${{ steps.aws_hosted_runners.outputs.labelmatrix }} + # steps: + # - name: AWS Hosted Runners + # id: aws_hosted_runners + # uses: cppalliance/aws-hosted-runners@v1.0.0 build: - needs: [ runner-selection ] + # needs: [ runner-selection ] defaults: run: shell: bash @@ -58,8 +63,7 @@ jobs: fail-fast: false matrix: include: - # Windows compilers - # + # Windows (3 configurations) - compiler: "msvc" version: "14.42" @@ -81,53 +85,9 @@ jobs: runs-on: "windows-2022" b2-toolset: "msvc-14.3" generator: "Visual Studio 17 2022" - is-latest: true - name: "MSVC 14.34: C++20" - shared: true - build-type: "Release" - build-cmake: true - - - compiler: "msvc" - version: "14.34" - cxxstd: "20" - latest-cxxstd: "20" - runs-on: "windows-2022" - b2-toolset: "msvc-14.3" - generator: "Visual Studio 17 2022" - is-latest: true - name: "MSVC 14.34: C++20 (x86)" - shared: false - x86: true - build-type: "Release" - - - compiler: "msvc" - version: "14.34" - cxxstd: "20" - latest-cxxstd: "20" - runs-on: "windows-2022" - b2-toolset: "msvc-14.3" - generator: "Visual Studio 17 2022" - is-latest: true - name: "MSVC 14.34: C++20" + name: "MSVC 14.34: C++20 (shared)" shared: true build-type: "Release" - build-cmake: true - - - compiler: "clang-cl" - version: "*" - cxxstd: "20" - latest-cxxstd: "20" - cxx: "clang++-cl" - cc: "clang-cl" - runs-on: "windows-2022" - b2-toolset: "clang-win" - generator-toolset: "ClangCL" - is-latest: true - is-earliest: true - name: "Windows-Clang: C++20" - shared: true - build-type: "Release" - build-cmake: true - compiler: "mingw" version: "*" @@ -140,29 +100,12 @@ jobs: generator: "MinGW Makefiles" is-latest: true is-earliest: true - name: "MinGW (shared): C++20" - shared: true - build-type: "Debug" - build-cmake: true - - - compiler: "mingw" - version: "*" - cxxstd: "20" - latest-cxxstd: "20" - cxx: "g++" - cc: "gcc" - runs-on: "windows-2022" - b2-toolset: "gcc" - generator: "MinGW Makefiles" - is-latest: true - is-earliest: true - name: "MinGW (static): C++20" + name: "MinGW: C++20" shared: false build-type: "Release" build-cmake: true - # OSX compilers - # + # macOS (2 configurations) - compiler: "apple-clang" version: "*" @@ -173,51 +116,11 @@ jobs: runs-on: "macos-26" b2-toolset: "clang" is-latest: true - name: "Apple-Clang (macOS 26): C++20" - shared: true - build-type: "Release" - build-cmake: true - - - compiler: "apple-clang" - version: "*" - cxxstd: "20" - latest-cxxstd: "20" - cxx: "clang++" - cc: "clang" - runs-on: "macos-26" - b2-toolset: "clang" - is-latest: true - name: "Apple-Clang (macOS 26, ubsan): C++20" - shared: false - build-type: "RelWithDebInfo" - ubsan: true - - - compiler: "apple-clang" - version: "*" - cxxstd: "20" - latest-cxxstd: "20" - cxx: "clang++" - cc: "clang" - runs-on: "macos-26" - b2-toolset: "clang" - is-latest: true - name: "Apple-Clang (macOS 26, asan): C++20" + name: "Apple-Clang (macOS 26, asan+ubsan): C++20" shared: true build-type: "RelWithDebInfo" asan: true - - - compiler: "apple-clang" - version: "*" - cxxstd: "20" - latest-cxxstd: "20" - cxx: "clang++" - cc: "clang" - runs-on: "macos-15" - b2-toolset: "clang" - name: "Apple-Clang (macOS 15): C++20" - shared: false - build-type: "Release" - build-cmake: true + ubsan: true - compiler: "apple-clang" version: "*" @@ -232,54 +135,7 @@ jobs: build-type: "Release" build-cmake: true - # Linux compilers - # - - - compiler: "gcc" - version: "15" - cxxstd: "20" - latest-cxxstd: "20" - cxx: "g++-15" - cc: "gcc-15" - runs-on: "ubuntu-latest" - container: "ubuntu:25.04" - b2-toolset: "gcc" - is-latest: true - name: "GCC 15: C++20" - shared: false - build-type: "Release" - build-cmake: true - - - compiler: "gcc" - version: "15" - cxxstd: "20" - latest-cxxstd: "20" - cxx: "g++-15" - cc: "gcc-15" - runs-on: "ubuntu-latest" - container: "ubuntu:25.04" - b2-toolset: "gcc" - is-latest: true - name: "GCC 15: C++20 (no zlib)" - shared: true - build-type: "Release" - build-cmake: true - - - compiler: "gcc" - version: "15" - cxxstd: "20" - latest-cxxstd: "20" - cxx: "g++-15" - cc: "gcc-15" - runs-on: "ubuntu-latest" - container: "ubuntu:25.04" - b2-toolset: "gcc" - is-latest: true - name: "GCC 15: C++20 (x86)" - shared: false - x86: true - build-type: "Release" - install: "gcc-15-multilib g++-15-multilib" + # Linux GCC (4 configurations) - compiler: "gcc" version: "15" @@ -306,98 +162,22 @@ jobs: container: "ubuntu:25.04" b2-toolset: "gcc" is-latest: true - name: "GCC 15: C++20 (x86)" - shared: false - x86: true - build-type: "Release" - install: "gcc-15-multilib g++-15-multilib" - build-cmake: true - - - compiler: "gcc" - version: "15" - cxxstd: "20" - latest-cxxstd: "20" - cxx: "g++-15" - cc: "gcc-15" - runs-on: "ubuntu-latest" - container: "ubuntu:25.04" - b2-toolset: "gcc" - is-latest: true - name: "GCC 15: C++20 (asan)" + name: "GCC 15: C++20 (asan+ubsan)" shared: true asan: true - build-type: "RelWithDebInfo" - - - compiler: "gcc" - version: "15" - cxxstd: "20" - latest-cxxstd: "20" - cxx: "g++-15" - cc: "gcc-15" - runs-on: "ubuntu-latest" - container: "ubuntu:25.04" - b2-toolset: "gcc" - is-latest: true - name: "GCC 15: C++20 (asan, x86)" - shared: false - asan: true - x86: true - build-type: "RelWithDebInfo" - install: "gcc-15-multilib g++-15-multilib" - - - compiler: "gcc" - version: "15" - cxxstd: "20" - latest-cxxstd: "20" - cxx: "g++-15" - cc: "gcc-15" - runs-on: "ubuntu-latest" - container: "ubuntu:25.04" - b2-toolset: "gcc" - is-latest: true - name: "GCC 15: C++20 (ubsan)" - shared: true ubsan: true build-type: "RelWithDebInfo" - compiler: "gcc" - version: "15" + version: "12" cxxstd: "20" latest-cxxstd: "20" - cxx: "g++-15" - cc: "gcc-15" + cxx: "g++-12" + cc: "gcc-12" runs-on: "ubuntu-latest" - container: "ubuntu:25.04" - b2-toolset: "gcc" - is-latest: true - name: "GCC 15: C++20 (ubsan, x86)" - shared: false - ubsan: true - x86: true - build-type: "RelWithDebInfo" - install: "gcc-15-multilib g++-15-multilib" - - - compiler: "gcc" - version: "14" - cxxstd: "20" - latest-cxxstd: "20" - cxx: "g++-14" - cc: "gcc-14" - runs-on: "ubuntu-24.04" - b2-toolset: "gcc" - name: "GCC 14: C++20" - shared: true - build-type: "Release" - - - compiler: "gcc" - version: "13" - cxxstd: "20" - latest-cxxstd: "20" - cxx: "g++-13" - cc: "gcc-13" - runs-on: "ubuntu-24.04" + container: "ubuntu:22.04" b2-toolset: "gcc" - name: "GCC 13: C++20" + name: "GCC 12: C++20" shared: true build-type: "Release" @@ -409,7 +189,6 @@ jobs: cc: "gcc-13" runs-on: "ubuntu-24.04" b2-toolset: "gcc" - is-latest: true name: "GCC 13: C++20 (coverage)" shared: false coverage: true @@ -418,31 +197,7 @@ jobs: ccflags: "--coverage -fprofile-arcs -ftest-coverage" install: "lcov wget unzip" - - compiler: "gcc" - version: "12" - cxxstd: "20" - latest-cxxstd: "20" - cxx: "g++-12" - cc: "gcc-12" - runs-on: "ubuntu-latest" - container: "ubuntu:22.04" - b2-toolset: "gcc" - name: "GCC 12: C++20" - shared: true - build-type: "Release" - - - compiler: "gcc" - version: "11" - cxxstd: "20" - latest-cxxstd: "20" - cxx: "g++-11" - cc: "gcc-11" - runs-on: "ubuntu-latest" - container: "ubuntu:22.04" - b2-toolset: "gcc" - name: "GCC 11: C++20" - shared: false - build-type: "Release" + # Linux Clang (5 configurations) - compiler: "clang" version: "20" @@ -459,22 +214,6 @@ jobs: build-type: "Release" build-cmake: true - - compiler: "clang" - version: "20" - cxxstd: "20,23" - latest-cxxstd: "23" - cxx: "clang++-20" - cc: "clang-20" - runs-on: "ubuntu-latest" - container: "ubuntu:24.04" - b2-toolset: "clang" - is-latest: true - name: "Clang 20: C++20-23 (x86)" - shared: false - x86: true - build-type: "Release" - install: "gcc-multilib g++-multilib" - - compiler: "clang" version: "20" cxxstd: "20" @@ -485,102 +224,12 @@ jobs: container: "ubuntu:24.04" b2-toolset: "clang" is-latest: true - name: "Clang 20: C++20 (time-trace)" - shared: true - time-trace: true - build-type: "Release" - cxxflags: "-ftime-trace" - ccflags: "-ftime-trace" - install: " wget unzip" - - - compiler: "clang" - version: "20" - cxxstd: "20" - latest-cxxstd: "20" - cxx: "clang++-20" - cc: "clang-20" - runs-on: "ubuntu-latest" - container: "ubuntu:24.04" - b2-toolset: "clang" - is-latest: true - name: "Clang 20: C++20 (asan)" + name: "Clang 20: C++20 (asan+ubsan)" shared: false asan: true - build-type: "RelWithDebInfo" - - - compiler: "clang" - version: "20" - cxxstd: "20" - latest-cxxstd: "20" - cxx: "clang++-20" - cc: "clang-20" - runs-on: "ubuntu-latest" - container: "ubuntu:24.04" - b2-toolset: "clang" - is-latest: true - name: "Clang 20: C++20 (asan, x86)" - shared: true - asan: true - x86: true - build-type: "RelWithDebInfo" - install: "gcc-multilib g++-multilib" - - - compiler: "clang" - version: "20" - cxxstd: "20,23" - latest-cxxstd: "23" - cxx: "clang++-20" - cc: "clang-20" - runs-on: "ubuntu-latest" - container: "ubuntu:24.04" - b2-toolset: "clang" - is-latest: true - name: "Clang 20: C++20-23 (ubsan)" - shared: false ubsan: true build-type: "RelWithDebInfo" - - compiler: "clang" - version: "20" - cxxstd: "20,23" - latest-cxxstd: "23" - cxx: "clang++-20" - cc: "clang-20" - runs-on: "ubuntu-latest" - container: "ubuntu:24.04" - b2-toolset: "clang" - is-latest: true - name: "Clang 20: C++20-23 (ubsan, x86)" - shared: true - ubsan: true - x86: true - build-type: "RelWithDebInfo" - install: "gcc-multilib g++-multilib" - - - compiler: "clang" - version: "19" - cxxstd: "20" - latest-cxxstd: "20" - cxx: "clang++-19" - cc: "clang-19" - runs-on: "ubuntu-24.04" - b2-toolset: "clang" - name: "Clang 19: C++20" - shared: false - build-type: "Release" - - - compiler: "clang" - version: "18" - cxxstd: "20" - latest-cxxstd: "20" - cxx: "clang++-18" - cc: "clang-18" - runs-on: "ubuntu-24.04" - b2-toolset: "clang" - name: "Clang 18: C++20" - shared: true - build-type: "Release" - - compiler: "clang" version: "17" cxxstd: "20" @@ -593,31 +242,6 @@ jobs: shared: false build-type: "Release" - - compiler: "clang" - version: "16" - cxxstd: "20" - latest-cxxstd: "20" - cxx: "clang++-16" - cc: "clang-16" - runs-on: "ubuntu-24.04" - b2-toolset: "clang" - name: "Clang 16: C++20" - shared: true - build-type: "Release" - - - compiler: "clang" - version: "15" - cxxstd: "20" - latest-cxxstd: "20" - cxx: "clang++-15" - cc: "clang-15" - runs-on: "ubuntu-latest" - container: "ubuntu:22.04" - b2-toolset: "clang" - name: "Clang 15: C++20" - shared: false - build-type: "Release" - - compiler: "clang" version: "14" cxxstd: "20" @@ -631,8 +255,26 @@ jobs: shared: true build-type: "Release" + - compiler: "clang" + version: "20" + cxxstd: "20,23" + latest-cxxstd: "23" + cxx: "clang++-20" + cc: "clang-20" + runs-on: "ubuntu-latest" + container: "ubuntu:24.04" + b2-toolset: "clang" + is-latest: true + name: "Clang 20: C++20-23 (x86)" + shared: false + x86: true + build-type: "Release" + install: "gcc-multilib g++-multilib" + name: ${{ matrix.name }} - runs-on: ${{ fromJSON(needs.runner-selection.outputs.labelmatrix)[matrix.runs-on] }} + # Skip self-hosted runner selection for now + # runs-on: ${{ fromJSON(needs.runner-selection.outputs.labelmatrix)[matrix.runs-on] }} + runs-on: ${{ matrix.runs-on }} container: image: ${{ matrix.container }} options: --privileged @@ -906,13 +548,15 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY changelog: - needs: [ runner-selection ] + # needs: [ runner-selection ] defaults: run: shell: bash name: Changelog Summary - runs-on: ${{ fromJSON(needs.runner-selection.outputs.labelmatrix)['ubuntu-22.04'] }} + # Skip self-hosted runner selection for now + # runs-on: ${{ fromJSON(needs.runner-selection.outputs.labelmatrix)['ubuntu-22.04'] }} + runs-on: 'ubuntu-22.04' timeout-minutes: 120 steps: @@ -931,16 +575,11 @@ jobs: tag-pattern: 'boost-.*\..*\..*' antora: - needs: [ runner-selection ] - strategy: - fail-fast: false - matrix: - include: - - { name: Windows, os: windows-latest } - - { name: Ubuntu, os: ubuntu-latest } - - { name: MacOS, os: macos-15 } - name: Antora Docs (${{ matrix.name }}) - runs-on: ${{ fromJSON(needs.runner-selection.outputs.labelmatrix)[matrix.os] }} + # needs: [ runner-selection ] + name: Antora Docs + # Skip self-hosted runner selection for now + # runs-on: ${{ fromJSON(needs.runner-selection.outputs.labelmatrix)['ubuntu-latest'] }} + runs-on: 'ubuntu-latest' defaults: run: shell: bash @@ -997,28 +636,23 @@ jobs: # Patch boost-root with workspace module cp -r "$workspace_root"/capy-root "libs/$module" - - uses: actions/setup-node@v4 with: node-version: 18 - - name: Setup Ninja - if: runner.os == 'Windows' - uses: seanmiddleditch/gha-setup-ninja@v5 - - name: Build Antora Docs env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | git config --global --add safe.directory "$(pwd)" - + BOOST_SRC_DIR="$(pwd)/boost-root" export BOOST_SRC_DIR cd boost-root/libs/capy - + cd doc bash ./build_antora.sh - + # Antora returns zero even if it fails, so we check if the site directory exists if [ ! -d "build/site" ]; then echo "Antora build failed" @@ -1028,5 +662,5 @@ jobs: - name: Create Antora Docs Artifact uses: actions/upload-artifact@v4 with: - name: antora-docs-${{ matrix.name }} + name: antora-docs path: boost-root/libs/capy/doc/build/site diff --git a/.gitignore b/.gitignore index a75e0d6..c2d9954 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /.vscode/ +/.cache/ /build/* !/build/Jamfile !/build/brotli.jam