diff --git a/fibers/BUILD b/fibers/BUILD index 0bb0b7e..d6e0430 100644 --- a/fibers/BUILD +++ b/fibers/BUILD @@ -23,16 +23,15 @@ load("@rules_cc//cc:defs.bzl", "cc_library", "cc_test") cc_library( - name = "stackpool", - srcs = [ - "stackpool.cc", - ] + select({ - "@platforms//os:linux": ["stackpool_linux.cc"], + name = "stackarena", + srcs = select({ + "//platforms:linux_x86_64": ["stackarena_linux_x86_64.cc"], }), - hdrs = ["stackpool.h"], + hdrs = ["stackarena.h"], deps = [ "@abseil-cpp//absl/status", "@abseil-cpp//absl/status:statusor", + "@abseil-cpp//absl/synchronization", ], linkopts = ["-latomic"], copts = ["-mcx16"], @@ -49,10 +48,10 @@ cc_library( ) cc_test( - name = "stackpool_test", - srcs = ["stackpool_test.cc"], + name = "stackarena_test", + srcs = ["stackarena_test.cc"], deps = [ - ":stackpool", + ":stackarena", "@abseil-cpp//absl/status", "@abseil-cpp//absl/status:statusor", "@abseil-cpp//absl/status:status_matchers", diff --git a/fibers/stackarena.h b/fibers/stackarena.h new file mode 100644 index 0000000..85c34b3 --- /dev/null +++ b/fibers/stackarena.h @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Copyright 2026 Adrian Gjerstad. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// ----------------------------------------------------------------------------- +// loom/fibers/stackarena.h +// ----------------------------------------------------------------------------- +// +// Fibers require stacks to run on, and in a performance-minded fiber +// implementation, stacks are expensive to allocate. A stack arena provides a +// single, massive, pre-allocated space in memory from which smaller stacks may +// be leased. +// +// Stacks returned as a result of `loom::StackArena::Lease()` all have the +// following properties: +// +// - Unique (system is battle-tested against race conditions involving multiple +// threads). +// - Aligned to a multiple of the system's memory page size. +// - *Never* executable. +// - Likely to be provided at an address that is advantageous for cache +// locality. +// + +#ifndef LOOM_FIBERS_STACKARENA_H_ +#define LOOM_FIBERS_STACKARENA_H_ + +#include +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/synchronization/mutex.h" + +namespace loom { + +class StackArena { + public: + // Copying is disallowed, as doing so would result in double-free upon + // destruction. + StackArena(const StackArena& other) = delete; + StackArena& operator=(const StackArena& other) = delete; + + // Moving is allowed. + StackArena(StackArena&& other) noexcept; + StackArena& operator=(StackArena&& other) noexcept; + + // Virtual to allow for mocks and fakes. + virtual ~StackArena(); + + // Creates a new loom::StackArena where each stack is of the given size + // (aligned to system page boundaries, in favor of more memory). + // + // Fails if the allocator could not allocate enough memory to support + // allocation of the initial chunk of memory. + static absl::StatusOr Create(size_t stack_size); + + // Attempts to obtain a new stack lease from the arena. Returns the pointer to + // the address of the lowest byte in the stack. May fail if the allocator + // could not allocate enough memory to support the operation. + virtual absl::StatusOr Lease(); + + // Marks a given stack lease as unneeded. Summarily equivalent to UNIX + // `free()`. It is undefined behavior to pass a pointer to this function that + // was not previously returned by `Lease()` or has already been passed to + // `Release()`. + virtual void Release(void* stack_base); + + // Getters + size_t stack_size() const { return stack_size_; } + + protected: + // Invoked by `Create()` to create the initial `StackArena`. No alignment + // checks are performed on stack_size. + explicit StackArena(size_t stack_size); + + private: + // Contains metadata about a chunk acquired by the allocator. + struct Chunk { + void* memory; + size_t size; + }; + + // A single stack in a free list is referenced by this Node struct. + struct Node { + Node* next; + }; + + // The free list is accessed like this to prevent ABA races. + struct TaggedNode { + Node* node; + uintptr_t tag; + }; + + // Obtains a new chunk of memory for use for stacks. Will fail if the + // allocator cannot obtain enough memory. + absl::Status CreateNewChunk(); + + size_t stack_size_; + + // This list is kept for clerical purposes, mostly cleanup at the destructor. + absl::Mutex chunk_mutex_; + std::vector chunks_; + + alignas(16) std::atomic free_list_head_{TaggedNode{nullptr, 0}}; +}; + +} + +#endif // LOOM_FIBERS_STACKARENA_H_ + diff --git a/fibers/stackarena_linux_x86_64.cc b/fibers/stackarena_linux_x86_64.cc new file mode 100644 index 0000000..b832527 --- /dev/null +++ b/fibers/stackarena_linux_x86_64.cc @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Copyright 2026 Adrian Gjerstad. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// ----------------------------------------------------------------------------- +// loom/fibers/stackarena_linux_x86_64.cc +// ----------------------------------------------------------------------------- +// +// Linux on x86_64 implementation of `loom::StackArena`. +// + +#include "fibers/stackarena.h" + +#include +#include +#include + +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/synchronization/mutex.h" + +namespace loom { + +namespace { + +// Each chunk can only be so big, and we should avoid allocating a bajillion of +// them. This variable dictates how many stacks are supposed to fit into each +// chunk of contiguous memory. +constexpr size_t kStacksPerChunk = 16; + +// Syscalls like `mmap` have arguments that are required to be aligned to a +// multiple of the system's page size. +const size_t kPageSize = sysconf(_SC_PAGESIZE); + +} + +StackArena::StackArena(StackArena&& other) noexcept + : stack_size_(other.stack_size_), chunks_(other.chunks_), + free_list_head_(other.free_list_head_.load(std::memory_order_relaxed)) { + absl::MutexLock lock(other.chunk_mutex_); + other.chunks_.clear(); + other.free_list_head_.store(StackArena::TaggedNode{nullptr, 0}); +} + +StackArena& StackArena::operator=(StackArena&& other) noexcept { + // Make `this` safe to move into first (release chunks) + absl::MutexLock lock(chunk_mutex_); + absl::MutexLock otherlock(other.chunk_mutex_); + + free_list_head_.store(StackArena::TaggedNode{nullptr, 0}); + for (const auto& chunk : chunks_) { + munmap(chunk.memory, chunk.size); + } + chunks_.clear(); + + // Now we can perform the destructive move. + stack_size_ = other.stack_size_; + chunks_ = other.chunks_; + free_list_head_.store(other.free_list_head_.load(std::memory_order_relaxed)); + other.chunks_.clear(); + other.free_list_head_.store(StackArena::TaggedNode{nullptr, 0}); + + return *this; +} + +StackArena::~StackArena() { + absl::MutexLock lock(chunk_mutex_); + + free_list_head_.store(StackArena::TaggedNode{nullptr, 0}); + + for (const auto& chunk : chunks_) { + munmap(chunk.memory, chunk.size); + } +} + +absl::StatusOr StackArena::Create(size_t stack_size) { + // Align stack_size to page boundary. + stack_size = (stack_size + kPageSize - 1) & ~(kPageSize - 1); + + // We have to have at least one page of usable memory. + if (stack_size <= kPageSize) { + stack_size = 2 * kPageSize; + } + + StackArena arena(stack_size); + auto status = arena.CreateNewChunk(); + if (!status.ok()) { + return status; + } + + return std::move(arena); +} + +absl::StatusOr StackArena::Lease() { + StackArena::TaggedNode old_head = + free_list_head_.load(std::memory_order_acquire); + + while (old_head.node != nullptr) { + // Access the 'next' pointer stored in the stack itself + // Note: Node is stored at the beginning of the usable stack memory + StackArena::Node* next_ptr = + static_cast(old_head.node)->next; + StackArena::TaggedNode new_head = {next_ptr, old_head.tag + 1}; + + if (free_list_head_.compare_exchange_weak(old_head, new_head, + std::memory_order_release, + std::memory_order_acquire)) { + // Tell the kernel that we will need this memory soon. + madvise(old_head.node, stack_size_, MADV_WILLNEED); + return static_cast(old_head.node); + } + // on failure, old_head is updated with the current value + } + + // No free stacks in any current chunk + auto status = CreateNewChunk(); + if (!status.ok()) { + return status; + } + + return Lease(); +} + +void StackArena::Release(void* stack_base) { + // Allow kernel to reclaim physical memory but keep virtual reservation + madvise(stack_base, stack_size_, MADV_DONTNEED); + + StackArena::Node* new_node = static_cast(stack_base); + StackArena::TaggedNode old_head = + free_list_head_.load(std::memory_order_acquire); + StackArena::TaggedNode new_head; + + do { + new_node->next = old_head.node; + new_head = {new_node, old_head.tag + 1}; + } while (!free_list_head_.compare_exchange_weak(old_head, new_head, + std::memory_order_release, + std::memory_order_acquire)); +} + +StackArena::StackArena(size_t stack_size) : stack_size_(stack_size) {} + +absl::Status StackArena::CreateNewChunk() { + absl::MutexLock lock(chunk_mutex_); + + size_t chunk_size = stack_size_ * kStacksPerChunk; + + void* base = mmap(nullptr, chunk_size, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + + if (base == MAP_FAILED) { + switch (errno) { + case EAGAIN: + case ENOMEM: + case EOVERFLOW: + return absl::ResourceExhaustedError("not enough memory for allocation"); + default: + return absl::UnknownError("mmap failed unexpectedly"); + } + } + + // Push the new stacks into the free list + for (size_t i = 0; i < kStacksPerChunk; ++i) { + void* stack = static_cast(base) + (i * stack_size_); + Release(stack); + } + + chunks_.push_back(StackArena::Chunk{base, chunk_size}); + return absl::OkStatus(); +} + +} + diff --git a/fibers/stackarena_test.cc b/fibers/stackarena_test.cc new file mode 100644 index 0000000..6bd5832 --- /dev/null +++ b/fibers/stackarena_test.cc @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Copyright 2026 Adrian Gjerstad. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// ----------------------------------------------------------------------------- +// loom/fibers/stackarena_test.cc +// ----------------------------------------------------------------------------- +// +// Tests for StackArena to ensure that an arena: +// +// A) Can be allocated +// B) Can lease and release stacks +// C) Leases out stacks in a FIFO pattern +// D) Holds up to multithreaded use. +// + +#include "fibers/stackarena.h" + +#include +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/status/status_matchers.h" +#include + +namespace loom { + +namespace { + +using ::absl_testing::IsOk; +using ::absl_testing::StatusIs; +using ::testing::Not; +using absl::StatusCode; + +class StackArenaTest : public ::testing::Test { + protected: + const uintptr_t kStackSize = 64 * 1024; // 64KB +}; + +TEST_F(StackArenaTest, AllocationSucceeds) { + auto arena_or = StackArena::Create(kStackSize); + ASSERT_THAT(arena_or, IsOk()); +} + +TEST_F(StackArenaTest, LeaseAndReleaseHappyPath) { + auto arena_or = StackArena::Create(kStackSize); + ASSERT_THAT(arena_or, IsOk()); + StackArena arena = std::move(arena_or).value(); + + // Should be able to lease + EXPECT_THAT(arena.Lease(), IsOk()); +} + +TEST_F(StackArenaTest, LIFOBehaviorForCacheLocality) { + auto arena_or = StackArena::Create(kStackSize); + ASSERT_THAT(arena_or, IsOk()); + StackArena arena = std::move(arena_or).value(); + + void* first = arena.Lease().value(); + void* second = arena.Lease().value(); + + // If we release 'second' then 'first', the next lease + // should give us 'first' back (LIFO). + arena.Release(second); + arena.Release(first); + + EXPECT_EQ(arena.Lease().value(), first); + EXPECT_EQ(arena.Lease().value(), second); +} + +TEST_F(StackArenaTest, MultithreadedHammerTest) { + auto arena_or = StackArena::Create(kStackSize); + ASSERT_THAT(arena_or, IsOk()); + StackArena arena = std::move(arena_or).value(); + + std::atomic running{true}; + auto initial_stack_or = arena.Lease(); + ASSERT_THAT(initial_stack_or, IsOk()); + arena.Release(initial_stack_or.value()); + + // One thread constantly leases/releases to rotate the TaggedNode tag + std::thread hammer([&]() { + while (running) { + auto s_or = arena.Lease(); + ASSERT_THAT(s_or, IsOk()); + arena.Release(s_or.value()); + } + }); + + // Main thread checks that we never get a duplicate pointer in the same cycle + for (int i = 0; i < 100000; ++i) { + auto s1_or = arena.Lease(); + ASSERT_THAT(s1_or, IsOk()); + auto s2_or = arena.Lease(); + ASSERT_THAT(s2_or, IsOk()); + EXPECT_NE(s1_or.value(), s2_or.value()); + arena.Release(s1_or.value()); + arena.Release(s2_or.value()); + } + + running = false; + hammer.join(); +} + +} // namespace + +} // namespace loom + +int main(int argc, char** argv) { + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} + diff --git a/fibers/stackpool.cc b/fibers/stackpool.cc deleted file mode 100644 index e1bc3b4..0000000 --- a/fibers/stackpool.cc +++ /dev/null @@ -1,84 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -// Copyright 2026 Adrian Gjerstad. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// ----------------------------------------------------------------------------- -// loom/fibers/stackpool.cc -// ----------------------------------------------------------------------------- -// -// Cross-platform components of a StackPool. -// - -#include "fibers/stackpool.h" - -#include -#include - -#include "absl/status/status.h" -#include "absl/status/statusor.h" - -namespace loom { - -StackPool::StackPool(void* pool_mem, unsigned int stacks, uintptr_t stack_size) - : free_list_head_({static_cast(pool_mem), 0}), - stack_size_(stack_size), pool_base_(pool_mem), - pool_size_bytes_(stack_size * stacks) { - // Set up free list - StackPoolNode* node = free_list_head_.load(std::memory_order_relaxed).ptr; - for (unsigned int i = 0; i < stacks - 1; ++i) { - node->next = reinterpret_cast((uintptr_t)node + stack_size); - node = node->next; - } - - node->next = NULL; -} - -absl::StatusOr StackPool::Lease() { - TaggedPointer old_head = free_list_head_.load(std::memory_order_acquire); - - while (old_head.ptr != nullptr) { - // We are safe to read .next because old_head.ptr is 'acquired' - StackPoolNode* next_node = old_head.ptr->next; - - TaggedPointer new_head = {next_node, old_head.tag + 1}; - - if (free_list_head_.compare_exchange_weak(old_head, new_head, - std::memory_order_acq_rel, - std::memory_order_acquire)) { - return static_cast(old_head.ptr); - } - // If it fails, old_head is updated with the current (newer) tag/ptr - } - - return absl::ResourceExhaustedError("no available stack"); -} - -void StackPool::Release(void* stack) { - auto* node = static_cast(stack); - TaggedPointer old_head = free_list_head_.load(std::memory_order_relaxed); - TaggedPointer new_head; - - do { - node->next = old_head.ptr; - new_head = {node, old_head.tag + 1}; - - // We use release to ensure node->next is visible before head_ changes - } while (!free_list_head_.compare_exchange_weak(old_head, new_head, - std::memory_order_release, - std::memory_order_relaxed)); -} - -} - diff --git a/fibers/stackpool.h b/fibers/stackpool.h deleted file mode 100644 index 6630a45..0000000 --- a/fibers/stackpool.h +++ /dev/null @@ -1,126 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -// Copyright 2026 Adrian Gjerstad. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// ----------------------------------------------------------------------------- -// loom/fibers/stackpool.h -// ----------------------------------------------------------------------------- -// -// Fibers require stacks to run on, and in a performance-minded fiber -// implementation, stacks are expensive to allocate. A stack pool provides a -// single, massive, pre-allocated space in memory from which smaller stacks may -// be leased. -// - -#ifndef LOOM_FIBERS_FIBERS_STACKPOOL_H_ -#define LOOM_FIBERS_FIBERS_STACKPOOL_H_ - -#include -#include - -#include "absl/status/statusor.h" - -namespace loom { - -// loom::StackPoolNode -// -// A node in a StackPool's free list. The physical memory address at which these -// nodes are placed are the bottom of each referenced stack. -struct StackPoolNode { - StackPoolNode* next; -}; - -// loom::TaggedPointer -// -// A reference into the stack pool free list that contains a unique tag, -// allowing for perfect memory order access without locks. -struct TaggedPointer { - StackPoolNode* ptr; - uintptr_t tag; -}; - -// loom::StackPool -// -// A resource pool that manages a huge amount of memory and can divvy it up into -// individual sections of a set size for use as stacks that fibers run on. -class StackPool { - public: - // Not default constructible. - StackPool() = delete; - - // Not copyable. - StackPool(const StackPool& other) = delete; - StackPool& operator=(const StackPool& other) = delete; - - // Moveable. - StackPool(StackPool&& other) - : free_list_head_(other.free_list_head_.load(std::memory_order_relaxed)), - stack_size_(other.stack_size_), pool_base_(other.pool_base_), - pool_size_bytes_(other.pool_size_bytes_) {} - StackPool& operator=(StackPool&& other) = default; - - // Creates a stack pool with the given metadata and a pre-allocated region of - // memory to use to back the pool. pool_mem MUST be allocated using the OS's - // specific way of allocating large blocks of memory. For this reason, - // StackPool::AllocateStackPool is provided to hide all of the system-specific - // logic and error handling. - StackPool(void* pool_mem, unsigned int stacks, uintptr_t stack_size); - - // Releases all stacks allocated by this pool back to the kernel. Please note - // that the memory released is not overwritten, only freed. - ~StackPool(); - - // Allocates a new stack pool with the given number of stacks, each of - // stack_size length (in bytes). Use this method instead of the constructor. - static absl::StatusOr AllocateStackPool(unsigned int stacks, - uintptr_t stack_size); - - // Lease() - // - // Asks the StackPool for a stack to use for a fiber. If the StackPool has one - // available, it will return a pointer to the bottom of the stack. If it does - // not, Lease() will fail with absl::StatusCode::kResourceExhausted. - // - // To obtain the size of the stacks that this pool provides, stack_size() is - // available. - absl::StatusOr Lease(); - - // Release() - // - // Informs the StackPool that the given stack is no longer needed and should - // be freed up for another fiber to use. - // - // It results in undefined behavior to pass a pointer that is not the base of - // a fixed-size stack to this method. - void Release(void* stack); - - // Obtains the size of stacks returned by Lease(). - uintptr_t stack_size() const { - return stack_size_; - } - - private: - alignas(16) std::atomic free_list_head_; - uintptr_t stack_size_; - - // Used for munmap, VirtualFree, etc. - void* pool_base_; - uintptr_t pool_size_bytes_; -}; - -} - -#endif // LOOM_FIBERS_FIBERS_STACKPOOL_H_ - diff --git a/fibers/stackpool_linux.cc b/fibers/stackpool_linux.cc deleted file mode 100644 index d4b5d6b..0000000 --- a/fibers/stackpool_linux.cc +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -// Copyright 2026 Adrian Gjerstad. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// ----------------------------------------------------------------------------- -// loom/fibers/stackpool_linux.cc -// ----------------------------------------------------------------------------- -// -// Implements the Linux-specific functionality for loom::StackPool. -// - -#include "fibers/stackpool.h" - -#include -#include - -#include - -#include "absl/status/status.h" -#include "absl/status/statusor.h" - -#include - -namespace loom { - -absl::StatusOr StackPool::AllocateStackPool(unsigned int stacks, - uintptr_t stack_size) { - // In Linux, we use mmap(2) to allocate the backing memory for stack pools. - // This gives us more control over stack alignments and access control. - void* mem = mmap(NULL, stack_size * stacks, PROT_READ | PROT_WRITE, - MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); - - if (mem == MAP_FAILED) { - switch (errno) { - case EAGAIN: - case ENOMEM: - case EOVERFLOW: - return absl::ResourceExhaustedError("not enough memory for allocation"); - case EINVAL: - return absl::InvalidArgumentError("size not aligned to page boundary"); - default: - return absl::UnknownError("mmap failed unexpectedly"); - } - } - - // StackPool must be constructed in place because it is move-only. - return absl::StatusOr(absl::in_place, mem, stacks, stack_size); -} - -StackPool::~StackPool() { - munmap(pool_base_, pool_size_bytes_); -} - -} - diff --git a/fibers/stackpool_test.cc b/fibers/stackpool_test.cc deleted file mode 100644 index 9208eca..0000000 --- a/fibers/stackpool_test.cc +++ /dev/null @@ -1,144 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -// Copyright 2026 Adrian Gjerstad. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// ----------------------------------------------------------------------------- -// loom/fibers/stackpool_test.cc -// ----------------------------------------------------------------------------- -// -// Tests for StackPool to ensure that a pool: -// -// A) Can be allocated -// B) Can lease and release stacks -// C) Leases out stacks in a FIFO pattern -// D) Holds up to multithreaded use. -// - -#include "fibers/stackpool.h" - -#include -#include -#include - -#include "absl/status/status.h" -#include "absl/status/statusor.h" -#include "absl/status/status_matchers.h" -#include - -namespace loom { - -namespace { - -using ::absl_testing::IsOk; -using ::absl_testing::StatusIs; -using ::testing::Not; -using absl::StatusCode; - -class StackPoolTest : public ::testing::Test { - protected: - const unsigned int kNumStacks = 10; - const uintptr_t kStackSize = 64 * 1024; // 64KB -}; - -TEST_F(StackPoolTest, AllocationSucceeds) { - auto pool_or = StackPool::AllocateStackPool(kNumStacks, kStackSize); - ASSERT_THAT(pool_or, IsOk()); -} - -TEST_F(StackPoolTest, LeaseAndReleaseHappyPath) { - auto pool_or = StackPool::AllocateStackPool(kNumStacks, kStackSize); - StackPool pool = std::move(pool_or).value(); - - // Lease all available stacks - std::vector leased_stacks; - for (unsigned int i = 0; i < kNumStacks; ++i) { - auto stack_or = pool.Lease(); - ASSERT_THAT(stack_or, IsOk()) << "Failed to lease stack " << i; - leased_stacks.push_back(stack_or.value()); - } - - // Next lease should fail - auto failed_lease = pool.Lease(); - EXPECT_THAT(failed_lease, StatusIs(StatusCode::kResourceExhausted)); - - // Release them all back - for (void* ptr : leased_stacks) { - pool.Release(ptr); - } - - // Should be able to lease again now - EXPECT_THAT(pool.Lease(), IsOk()); -} - -TEST_F(StackPoolTest, LIFOBehaviorForCacheLocality) { - auto pool_or = StackPool::AllocateStackPool(kNumStacks, kStackSize); - StackPool pool = std::move(pool_or).value(); - - void* first = pool.Lease().value(); - void* second = pool.Lease().value(); - - // If we release 'second' then 'first', the next lease - // should give us 'first' back (LIFO). - pool.Release(second); - pool.Release(first); - - EXPECT_EQ(pool.Lease().value(), first); - EXPECT_EQ(pool.Lease().value(), second); -} - -TEST_F(StackPoolTest, MultithreadedHammerTest) { - const int kThreads = 8; - const int kIterations = 1000; - auto pool_or = StackPool::AllocateStackPool(kThreads * 2, kStackSize); - StackPool pool = std::move(pool_or).value(); - - auto worker = [&pool, kIterations]() { - for (int i = 0; i < kIterations; ++i) { - auto stack_or = pool.Lease(); - if (stack_or.ok()) { - void* ptr = stack_or.value(); - // Mimic some "work" being done on the stack - std::this_thread::yield(); - pool.Release(ptr); - } - } - }; - - std::vector threads; - for (int i = 0; i < kThreads; ++i) { - threads.emplace_back(worker); - } - - for (auto& t : threads) { - t.join(); - } - - // After all that hammering, we should still be able to lease - // exactly the number of stacks we started with. - for (unsigned int i = 0; i < kThreads * 2; ++i) { - EXPECT_THAT(pool.Lease(), IsOk()); - } - EXPECT_THAT(pool.Lease(), Not(IsOk())); -} - -} // namespace - -} // namespace loom - -int main(int argc, char** argv) { - testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} - diff --git a/fibers/stackswitch.h b/fibers/stackswitch.h index c26c3e0..fd6f125 100644 --- a/fibers/stackswitch.h +++ b/fibers/stackswitch.h @@ -45,14 +45,14 @@ extern "C" { // pointer valid as the first argument to loom__switch_to_stack. // // For example: -// auto stack_or = pool.Lease(); +// auto stack_or = arena.Lease(); // if (!stack_or.ok()) { // // Failure logic // } // void* stack = stack_or.value(); // // // Configure stack -// void* sp = loom::ConfigreStack(stack, pool.stack_size(), FiberMain, NULL); +// void* sp = loom::ConfigreStack(stack, arena.stack_size(), FiberMain, NULL); // // // Start fiber // loom::SwitchStack(sp, &this_sp); diff --git a/fibers/stackswitch_test.cc b/fibers/stackswitch_test.cc index a6a1401..6c29f4b 100644 --- a/fibers/stackswitch_test.cc +++ b/fibers/stackswitch_test.cc @@ -28,7 +28,8 @@ // E) Aborts when an entry point returns (see #3) // F) Includes SIMD saving when necessary. // -// This test statically allocates its stacks to make it separate from StackPool. +// This test statically allocates its stacks to make it separate from +// StackArena. // #include "fibers/stackswitch.h"