Serene Runtime 1.0.0-dev
C runtime for the Serene programming language
Loading...
Searching...
No Matches
fiber_tests.h
Go to the documentation of this file.
1/* -*- C -*-
2 * Serene programming language
3 * Copyright (C) 2019-2026 Sameer Rahmani <lxsameer@lxsameer.com>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 */
18
19#pragma once
20
21#include <stdatomic.h>
22#include <stdint.h>
23#include <string.h>
24
25#include "base.h"
26#include "serene/rt/fiber.h"
29
30#define FIBER_TESTS(X) \
31 X("fiber::types", test_fiber_types), X("fiber::switch", test_fiber_switch), \
32 X("fiber::stack", test_fiber_stack), \
33 X("fiber::thread_stack_bounds", test_fiber_thread_stack_bounds), \
34 X("fiber::auto_name", test_fiber_auto_name), \
35 X("fiber::make_schedule", test_fiber_make_schedule), \
36 X("fiber::sched::auto_workers", test_fiber_sched_auto_workers), \
37 X("fiber::spawn_copy", test_fiber_spawn_copy), X("fiber::sched::run", test_fiber_sched_run), \
38 X("fiber::sched::yield", test_fiber_sched_yield), \
39 X("fiber::sched::yield_round_robin", test_fiber_sched_yield_round_robin), \
40 X("fiber::sched::suspend", test_fiber_sched_suspend), \
41 X("fiber::sched::park_abort", test_fiber_sched_park_abort), \
42 X("fiber::sched::registry", test_fiber_sched_registry), \
43 X("fiber::sched::double_wake", test_fiber_sched_double_wake), \
44 X("fiber::sched::wait_for", test_fiber_sched_wait_for), \
45 X("fiber::sched::wait_for_done", test_fiber_sched_wait_for_done), \
46 X("fiber::sched::wait_for_many", test_fiber_sched_wait_for_many), \
47 X("fiber::sched::mt_run", test_fiber_sched_mt_run), \
48 X("fiber::sched::mt_yield", test_fiber_sched_mt_yield), \
49 X("fiber::sched::stop", test_fiber_sched_stop)
50
51// A worker loop fiber must know its own stack bounds from init. A worker
52// that only ever resumes stolen fibers never launches a fresh fiber, so
53// srn_fiber_on_entry never reports its loop stack, and without the bounds a
54// switch back to the loop hands ASan a null stack. Only the ASan build
55// records them, the other builds have no reader.
57 srn_fiber_t loop;
60#if SRN_ASAN
61 TEST_CHECK(loop.stack.limit != nullptr);
62 TEST_CHECK(loop.stack.start != nullptr);
63 // A local's address cannot anchor the check, ASan's fake stack moves
64 // address taken locals off the real thread stack. Sanity of the reported
65 // range is what is checkable, ordered bounds spanning at least one page.
67#else
68 TEST_CHECK(loop.stack.limit == nullptr);
69#endif
70}
71
73 UNUSED(ctx);
74 UNUSED(arg);
75 return nullptr;
76}
77
79
81 UNUSED(arg);
83 return nullptr;
84}
85
86// Every fiber gets an autogenerated debug name at make time. Created on a
87// worker the tag is f#<worker>:<n>, the creating worker's id and its spawn
88// count. Created off the pool it is f#m:<id> from the engine wide object id
89// counter. Provenance, not placement, a stolen fiber runs elsewhere.
90static void test_fiber_auto_name() {
91 MAKE_ENGINE(mm, engine);
92 MAKE_CONTEXT(engine, ctx);
93 srn_scheduler_t *sched = engine->scheduler;
94
97 auto_name_child = nullptr;
98 (void)srn_fiber_spawn(ctx, auto_name_parent_entry, nullptr);
99
100 // Made off the pool, the creator tag is `m`.
101 TEST_CHECK(strncmp(a->name, "f#m:", 4) == 0);
102 TEST_CHECK(strncmp(b->name, "f#m:", 4) == 0);
103 TEST_CHECK(strcmp(a->name, b->name) != 0);
104
105 // Run the fibers so the registry is empty before the context goes away.
106 srn_sched_run(sched, 1);
107
108 // The child was made by the parent running on the single worker, id 0,
109 // and it is that worker's first spawn.
110 TEST_ASSERT(auto_name_child != nullptr);
111 TEST_CHECK(strcmp(auto_name_child->name, "f#0:1") == 0);
112
113 RELEASE_CONTEXT(ctx);
114 SHUTDOWN_ENGINE(mm, engine);
115}
116
117// srn_fiber_make leaves the fiber NEW and registered, srn_fiber_schedule
118// starts it exactly once. An unscheduled fiber never runs, does not block
119// quiescence, and is still reaped at shutdown.
121 MAKE_ENGINE(mm, engine);
122 MAKE_CONTEXT(engine, ctx);
123 srn_scheduler_t *sched = engine->scheduler;
124
125 srn_fiber_t *stay = srn_fiber_make(ctx, sched, "left-new", auto_name_entry, nullptr, 0);
126 srn_fiber_t *go = srn_fiber_make(ctx, sched, "scheduled", auto_name_entry, nullptr, 0);
127
129 TEST_CHECK(strcmp(go->name, "scheduled") == 0);
130 TEST_CHECK(strcmp(stay->name, "left-new") == 0);
131
133 srn_sched_run(sched, 1);
134
137
138 // Reap the never scheduled fiber while its struct (in the context block)
139 // is still alive, before releasing the context.
140 srn_sched_shutdown(sched);
141 RELEASE_CONTEXT(ctx);
142 SHUTDOWN_ENGINE(mm, engine);
143}
144
145typedef struct {
146 int value;
148
150
152 UNUSED(ctx);
153 spawn_copy_seen = ((spawn_copy_args_t *)arg)->value;
154 return nullptr;
155}
156
157// The copying spawn owns its argument, so the caller's stack frame can die
158// before the fiber runs. Passing the address of the scoped struct directly
159// would be a stack use after scope (the 12_http_server bug shape).
161 MAKE_ENGINE(mm, engine);
162 MAKE_CONTEXT(engine, ctx);
163 srn_scheduler_t *sched = engine->scheduler;
164
165 spawn_copy_seen = 0;
166 {
167 spawn_copy_args_t args = {.value = 4242};
169 }
170
171 srn_sched_run(sched, 1);
173
174 RELEASE_CONTEXT(ctx);
175 SHUTDOWN_ENGINE(mm, engine);
176}
177
178static atomic_int auto_workers_ran;
179
181 UNUSED(ctx);
182 UNUSED(arg);
183 atomic_fetch_add(&auto_workers_ran, 1);
184 return nullptr;
185}
186
187// A zero worker request with the delegating zero default resolves to the
188// CPU count at run time. The pool comes up, drains every fiber, and reaches
189// quiescence with however many workers the machine provided.
192
193 MAKE_ENGINE(mm, engine);
194 MAKE_CONTEXT(engine, ctx);
195 srn_scheduler_t *sched = engine->scheduler;
196
197 atomic_store(&auto_workers_ran, 0);
198 for (int i = 0; i < 32; i++) {
199 (void)srn_fiber_spawn(ctx, auto_workers_entry, nullptr);
200 }
201
202 srn_sched_run(sched, 0);
203 TEST_CHECK(atomic_load(&auto_workers_ran) == 32);
204
205 RELEASE_CONTEXT(ctx);
206 SHUTDOWN_ENGINE(mm, engine);
207}
208
209static void test_fiber_types() {
210 // Smoke test, the public types exist and the build/test wiring works.
211 // The saved context is a single stack-pointer word.
212 TEST_CHECK(sizeof(srn_fiber_ctx_t) == sizeof(void *));
213 // The lifecycle states run from creation to completion in order.
219}
220
221// The POSIX provider hands back a guard-paged region, `guard` is the mmap base
222// (the PROT_NONE page), `limit` is the low end of the usable area one page
223// above it, and `start` is the high end where the stack pointer begins.
224static void test_fiber_stack() {
225 size_t page = srn_mm_get_os_page_size();
226 size_t guard_pages = 4;
227
228 srn_fiber_stack_t s = srn_fiber_stack_alloc(0, guard_pages);
229
230 TEST_CHECK(s.guard != nullptr);
231 TEST_CHECK(s.limit != nullptr);
232 TEST_CHECK(s.start != nullptr);
233 // The guard band spans the requested number of pages below the usable region.
234 TEST_CHECK((char *)s.limit == (char *)s.guard + guard_pages * page);
235 // The usable region is non-empty, page-aligned, and at least the default.
236 TEST_CHECK((char *)s.start > (char *)s.limit);
237 TEST_CHECK(((uintptr_t)s.limit & (page - 1)) == 0);
239
240 // The whole usable region is writable end to end. The guard is left alone.
241 ((char *)s.limit)[0] = (char)0x5A;
242 ((char *)s.start)[-1] = (char)0x5A;
243
245}
246
247// Ping-pong, a worker fiber and the calling thread hand control back and forth.
248// This exercises the whole switch path -- srn_fiber_ctx_make seeds the worker,
249// the first srn_fiber_switch starts it via the trampoline, each yield resumes
250// the thread, and the worker finishes with srn_fiber_switch_final. The worker
251// runs on a real guard-paged stack from the provider.
254static int fiber_pp_ticks;
256
257static void fiber_pp_entry(void *arg) {
258 UNUSED(arg);
260 for (int i = 0; i < fiber_pp_rounds; i++) {
263 }
265}
266
267static void test_fiber_switch() {
269
271
273 fiber_pp_worker.stack = worker_stack;
275 srn_fiber_ctx_make(&fiber_pp_worker.fiber_ctx, worker_stack, fiber_pp_entry, nullptr);
276
277 fiber_pp_ticks = 0;
278 fiber_pp_rounds = 5;
279
280 // `rounds` yields, plus one more switch to let the worker run to completion.
281 for (int i = 0; i <= fiber_pp_rounds; i++) {
283 }
284
287
288 srn_fiber_stack_free(worker_stack);
289}
290
291// A shared return value, these tests do not inspect the payload, only that a
292// fiber ran and finished (a null result is legal too).
293static int fiber_ok_value;
294
295// fiber::sched::run -- spawn N fibers, each bumps a counter and returns. After
296// srn_sched_run drains the queue every fiber must have run once and reached
297// DONE, with its result recorded.
299
301 UNUSED(ctx);
302 UNUSED(arg);
304 return &fiber_ok_value;
305}
306
307static void test_fiber_sched_run() {
308 MAKE_ENGINE(mm, engine);
309 MAKE_CONTEXT(engine, ctx);
310 srn_scheduler_t *sched = engine->scheduler;
311 ASSERT_NOT_NULL(sched);
312
314 enum { N = 4 };
315 srn_fiber_t *fibers[N];
316 for (int i = 0; i < N; i++) {
317 fibers[i] = srn_fiber_spawn(ctx, fiber_run_entry, nullptr);
318 ASSERT_NOT_NULL(fibers[i]);
319 }
320
321 srn_sched_run(sched, 1);
322
324 for (int i = 0; i < N; i++) {
325 // The struct lives in the context block after reaping. Only the stack is
326 // released, so state and result are still readable here.
327 TEST_CHECK(fibers[i]->state == SRN_FIBER_DONE);
328 TEST_CHECK(fibers[i]->result == &fiber_ok_value);
329 }
330
331 RELEASE_CONTEXT(ctx);
332 SHUTDOWN_ENGINE(mm, engine);
333}
334
335// fiber::sched::yield -- two fibers each log their id twice, yielding between.
336// Run on one worker. Per-worker queues do not guarantee a strict interleaving,
337// so the invariant checked is that both fibers ran to completion, each id
338// appears exactly twice, four entries in all.
339static int fiber_yield_log[4];
340static int fiber_yield_n;
341
343 UNUSED(ctx);
344 int id = (int)(intptr_t)arg;
345 for (int i = 0; i < 2; i++) {
348 }
349 return &fiber_ok_value;
350}
351
353 MAKE_ENGINE(mm, engine);
354 MAKE_CONTEXT(engine, ctx);
355 srn_scheduler_t *sched = engine->scheduler;
356 ASSERT_NOT_NULL(sched);
357
358 fiber_yield_n = 0;
359 (void)srn_fiber_spawn(ctx, fiber_yield_entry, (void *)(intptr_t)1);
360 (void)srn_fiber_spawn(ctx, fiber_yield_entry, (void *)(intptr_t)2);
361
362 srn_sched_run(sched, 1);
363
365 int ones = 0;
366 int twos = 0;
367 for (int i = 0; i < 4; i++) {
368 if (fiber_yield_log[i] == 1) {
369 ones++;
370 } else if (fiber_yield_log[i] == 2) {
371 twos++;
372 }
373 }
374 TEST_CHECK(ones == 2);
375 TEST_CHECK(twos == 2);
376
377 RELEASE_CONTEXT(ctx);
378 SHUTDOWN_ENGINE(mm, engine);
379}
380
381// fiber::sched::yield_round_robin -- yield moves the fiber to the back of the
382// queue, so two yielding fibers on one worker interleave step by step. A
383// yield that re-runs its own fiber produces runs of the same id instead.
384static int fiber_rr_log[6];
385static int fiber_rr_n;
386
388 UNUSED(ctx);
389 int id = (int)(intptr_t)arg;
390 for (int i = 0; i < 3; i++) {
391 if (fiber_rr_n < 6) {
392 fiber_rr_log[fiber_rr_n++] = id;
393 }
395 }
396 return &fiber_ok_value;
397}
398
400 MAKE_ENGINE(mm, engine);
401 MAKE_CONTEXT(engine, ctx);
402 srn_scheduler_t *sched = engine->scheduler;
403 ASSERT_NOT_NULL(sched);
404
405 fiber_rr_n = 0;
406 (void)srn_fiber_spawn(ctx, fiber_rr_entry, (void *)(intptr_t)1);
407 (void)srn_fiber_spawn(ctx, fiber_rr_entry, (void *)(intptr_t)2);
408
409 srn_sched_run(sched, 1);
410
412 // Strict alternation, a yielding fiber never runs two steps back to back
413 // while a peer is ready.
414 for (int i = 1; i < 6; i++) {
416 TEST_MSG(
417 "order: %d %d %d %d %d %d", fiber_rr_log[0], fiber_rr_log[1], fiber_rr_log[2],
419 );
420 }
421
422 RELEASE_CONTEXT(ctx);
423 SHUTDOWN_ENGINE(mm, engine);
424}
425
426// fiber::sched::suspend -- a one-slot mailbox handoff. The consumer runs first
427// and parks. Its commit registers it as the waiter (the slot is empty). The
428// producer fills the slot and readies the consumer, which resumes and reads it.
429typedef struct {
430 int value;
434
436static int fiber_mbox_got;
437
438// Park commit, register as the mailbox waiter, unless a value is already there
439// (in which case decline to park so the consumer resumes at once).
440static bool fiber_mbox_park(srn_fiber_t *self, void *arg) {
441 fiber_mbox_t *mb = arg;
442 if (mb->has_value) {
443 return false;
444 }
445 mb->waiter = self;
446 return true;
447}
448
456
458 UNUSED(ctx);
459 UNUSED(arg);
460 fiber_mbox.value = 42;
461 fiber_mbox.has_value = true;
462 if (fiber_mbox.waiter != nullptr) {
463 srn_fiber_t *w = fiber_mbox.waiter;
464 fiber_mbox.waiter = nullptr;
466 }
467 return &fiber_ok_value;
468}
469
471 MAKE_ENGINE(mm, engine);
472 MAKE_CONTEXT(engine, ctx);
473 srn_scheduler_t *sched = engine->scheduler;
474 ASSERT_NOT_NULL(sched);
475
477 fiber_mbox_got = 0;
478
479 // Order matters, the consumer must be enqueued first so it runs and suspends
480 // before the producer fills the slot.
481 (void)srn_fiber_spawn(ctx, fiber_consumer_entry, nullptr);
482 (void)srn_fiber_spawn(ctx, fiber_producer_entry, nullptr);
483
484 srn_sched_run(sched, 1);
485
487
488 RELEASE_CONTEXT(ctx);
489 SHUTDOWN_ENGINE(mm, engine);
490}
491
492// fiber::sched::park_abort -- the commit declines to park. The value is already
493// in the slot when the consumer runs, so fiber_mbox_park returns false and the
494// consumer resumes immediately without ever parking or registering a waiter.
496 MAKE_ENGINE(mm, engine);
497 MAKE_CONTEXT(engine, ctx);
498 srn_scheduler_t *sched = engine->scheduler;
499 ASSERT_NOT_NULL(sched);
500
502 fiber_mbox.value = 7;
503 fiber_mbox.has_value = true; // value present before the consumer runs
504 fiber_mbox_got = 0;
505
506 (void)srn_fiber_spawn(ctx, fiber_consumer_entry, nullptr);
507
508 srn_sched_run(sched, 1);
509
511 TEST_CHECK(fiber_mbox.waiter == nullptr); // never registered as a waiter
512
513 RELEASE_CONTEXT(ctx);
514 SHUTDOWN_ENGINE(mm, engine);
515}
516
517// fiber::sched::registry -- a fiber that suspends with no one to wake it. The
518// run loop drains the ready queue and returns, leaving the fiber parked and its
519// stack mapped. The scheduler still tracks it through the registry, so shutdown
520// reclaims the stack instead of leaking it.
521// Park commit that registers the fiber nowhere, so nothing can ever wake it.
522static bool fiber_stuck_park(srn_fiber_t *self, void *arg) {
523 UNUSED(self);
524 UNUSED(arg);
525 return true; // stay parked, with no waker
526}
527
529 UNUSED(ctx);
530 UNUSED(arg);
531 srn_fiber_suspend(fiber_stuck_park, nullptr); // never readied
532 return &fiber_ok_value;
533}
534
536 MAKE_ENGINE(mm, engine);
537 MAKE_CONTEXT(engine, ctx);
538 srn_scheduler_t *sched = engine->scheduler;
539 ASSERT_NOT_NULL(sched);
540
541 srn_fiber_t *stuck = srn_fiber_spawn(ctx, fiber_stuck_entry, nullptr);
542 ASSERT_NOT_NULL(stuck);
543
544 // Drains the ready queue. The stuck fiber suspends and is abandoned.
545 srn_sched_run(sched, 1);
547
548 // The registry lets shutdown find and free the abandoned fiber's stack.
549 srn_sched_shutdown(sched);
550
551 RELEASE_CONTEXT(ctx);
552 SHUTDOWN_ENGINE(mm, engine);
553}
554
555// fiber::sched::double_wake -- two wakers ready the same suspended fiber (as an
556// IO event and a timeout might race). The wake must be idempotent, the fiber
557// runs exactly once, not enqueued twice. A double enqueue would resume an
558// already-reaped fiber on the second pass and trip the sanitizer.
562
563static bool fiber_dwake_park(srn_fiber_t *self, void *arg) {
564 UNUSED(arg);
566 return false;
567 }
568 fiber_dwake_waiter = self;
569 return true;
570}
571
573 UNUSED(ctx);
574 UNUSED(arg);
577 return &fiber_ok_value;
578}
579
581 UNUSED(ctx);
582 UNUSED(arg);
584 if (fiber_dwake_waiter != nullptr) {
586 fiber_dwake_waiter = nullptr;
587 srn_fiber_ready(w); // first waker: SUSPENDED -> READY, enqueued
588 srn_fiber_ready(w); // second waker: already READY -> no-op
589 }
590 return &fiber_ok_value;
591}
592
594 MAKE_ENGINE(mm, engine);
595 MAKE_CONTEXT(engine, ctx);
596 srn_scheduler_t *sched = engine->scheduler;
597 ASSERT_NOT_NULL(sched);
598
599 fiber_dwake_waiter = nullptr;
600 fiber_dwake_signalled = false;
602
603 // Consumer first, so it runs and suspends before the producer wakes it.
604 (void)srn_fiber_spawn(ctx, fiber_dwake_consumer_entry, nullptr);
605 (void)srn_fiber_spawn(ctx, fiber_dwake_producer_entry, nullptr);
606
607 srn_sched_run(sched, 1);
608
610
611 RELEASE_CONTEXT(ctx);
612 SHUTDOWN_ENGINE(mm, engine);
613}
614
615// fiber::sched::wait_for -- the caller blocks until the target finishes and
616// reads its result. The waiter runs first and parks. The target runs later,
617// finishes, and the DONE handling wakes the waiter.
618static int fiber_wf_result; // the target's result
619static srn_fiber_t *fiber_wf_target; // set before the run
620static srn_fiber_result_t fiber_wf_seen; // what the waiter read
621
623 UNUSED(ctx);
624 UNUSED(arg);
625 return &fiber_wf_result;
626}
627
634
636 MAKE_ENGINE(mm, engine);
637 MAKE_CONTEXT(engine, ctx);
638 srn_scheduler_t *sched = engine->scheduler;
639 ASSERT_NOT_NULL(sched);
640
641 fiber_wf_seen = nullptr;
642 // Waiter made first, so it runs and parks before the target finishes.
643 (void)srn_fiber_spawn(ctx, fiber_wf_waiter_entry, nullptr);
645
646 srn_sched_run(sched, 1);
647
649
650 RELEASE_CONTEXT(ctx);
651 SHUTDOWN_ENGINE(mm, engine);
652}
653
654// fiber::sched::wait_for_done -- waiting for an already-finished target returns
655// its result at once. The commit sees DONE and declines to park.
657 MAKE_ENGINE(mm, engine);
658 MAKE_CONTEXT(engine, ctx);
659 srn_scheduler_t *sched = engine->scheduler;
660 ASSERT_NOT_NULL(sched);
661
662 fiber_wf_seen = nullptr;
663 // Target made first, so it runs and finishes before the waiter runs.
665 (void)srn_fiber_spawn(ctx, fiber_wf_waiter_entry, nullptr);
666
667 srn_sched_run(sched, 1);
668
670
671 RELEASE_CONTEXT(ctx);
672 SHUTDOWN_ENGINE(mm, engine);
673}
674
675// fiber::sched::wait_for_many -- several fibers wait for one target. All of
676// them wake and read its result once it finishes.
677static int fiber_wf_woke;
678
680 UNUSED(ctx);
681 UNUSED(arg);
684 }
685 return &fiber_ok_value;
686}
687
689 MAKE_ENGINE(mm, engine);
690 MAKE_CONTEXT(engine, ctx);
691 srn_scheduler_t *sched = engine->scheduler;
692 ASSERT_NOT_NULL(sched);
693
694 fiber_wf_woke = 0;
695 // Three waiters made first so they all park, then the target finishes.
696 (void)srn_fiber_spawn(ctx, fiber_wf_counting_waiter, nullptr);
697 (void)srn_fiber_spawn(ctx, fiber_wf_counting_waiter, nullptr);
698 (void)srn_fiber_spawn(ctx, fiber_wf_counting_waiter, nullptr);
700
701 srn_sched_run(sched, 1);
702
704
705 RELEASE_CONTEXT(ctx);
706 SHUTDOWN_ENGINE(mm, engine);
707}
708
709// fiber::sched::mt_run -- many fibers across several worker threads, each doing
710// one atomic increment. With true parallelism the only safe shared state is the
711// atomic counter. After the run every increment must have landed (no lost
712// updates) and the pool must have reached quiescence, which is what makes
713// srn_sched_run return.
714static atomic_int fiber_mt_counter;
715
717 UNUSED(ctx);
718 UNUSED(arg);
719 atomic_fetch_add(&fiber_mt_counter, 1);
720 return &fiber_ok_value;
721}
722
724 MAKE_ENGINE(mm, engine);
725 MAKE_CONTEXT(engine, ctx);
726 srn_scheduler_t *sched = engine->scheduler;
727 ASSERT_NOT_NULL(sched);
728
729 atomic_store(&fiber_mt_counter, 0);
730 enum { N = 256 };
731 for (int i = 0; i < N; i++) {
732 (void)srn_fiber_spawn(ctx, fiber_mt_entry, nullptr);
733 }
734
735 srn_sched_run(sched, 4);
736
737 TEST_CHECK(atomic_load(&fiber_mt_counter) == N);
738
739 RELEASE_CONTEXT(ctx);
740 SHUTDOWN_ENGINE(mm, engine);
741}
742
743// fiber::sched::mt_yield -- fibers that yield repeatedly across worker threads,
744// so each is re-enqueued between rounds and may resume on a different thread
745// than it last ran on. Exercises the cross-thread re-enqueue and the park/wake
746// path under contention. The counter totals every round of every fiber.
747static atomic_int fiber_mt_yield_counter;
748
750 UNUSED(ctx);
751 int rounds = (int)(intptr_t)arg;
752 for (int i = 0; i < rounds; i++) {
753 atomic_fetch_add(&fiber_mt_yield_counter, 1);
755 }
756 return &fiber_ok_value;
757}
758
760 MAKE_ENGINE(mm, engine);
761 MAKE_CONTEXT(engine, ctx);
762 srn_scheduler_t *sched = engine->scheduler;
763 ASSERT_NOT_NULL(sched);
764
765 atomic_store(&fiber_mt_yield_counter, 0);
766 enum { N = 64, ROUNDS = 8 };
767 for (int i = 0; i < N; i++) {
768 (void)srn_fiber_spawn(ctx, fiber_mt_yield_entry, (void *)(intptr_t)ROUNDS);
769 }
770
771 srn_sched_run(sched, 4);
772
773 TEST_CHECK(atomic_load(&fiber_mt_yield_counter) == N * ROUNDS);
774
775 RELEASE_CONTEXT(ctx);
776 SHUTDOWN_ENGINE(mm, engine);
777}
778
779// fiber::sched::stop -- a fiber stops the scheduler before all the work is
780// done. The stopper runs a few rounds then calls srn_sched_stop, so
781// srn_sched_run returns instead of draining the queue. The pending fibers
782// behind it are left unrun, and srn_sched_shutdown reaps their stacks. Checks
783// the run returns rather than hangs, only the stopper ran, and the leftover
784// teardown is clean.
785static atomic_int fiber_stop_rounds;
786
788 UNUSED(ctx);
789 srn_scheduler_t *sched = arg;
790 // No yield in this loop, a yielded fiber goes to the back of the queue, so
791 // yielding would hand the pending fibers a turn before the stop lands.
792 for (int i = 0; i < 3; i++) {
793 atomic_fetch_add(&fiber_stop_rounds, 1);
794 }
795 srn_sched_stop(sched);
796 return &fiber_ok_value;
797}
798
800 UNUSED(ctx);
801 UNUSED(arg);
802 // Never reached, the stopper ends the run first. It adds a large amount, so
803 // any accidental execution is visible in the count.
804 atomic_fetch_add(&fiber_stop_rounds, 1000);
805 return &fiber_ok_value;
806}
807
809 MAKE_ENGINE(mm, engine);
810 MAKE_CONTEXT(engine, ctx);
811 srn_scheduler_t *sched = engine->scheduler;
812 ASSERT_NOT_NULL(sched);
813
814 atomic_store(&fiber_stop_rounds, 0);
815 // Stopper first so it reaches the single worker before the pending fibers.
816 (void)srn_fiber_spawn(ctx, fiber_stopper_entry, sched);
817 enum { PENDING = 3 };
818 for (int i = 0; i < PENDING; i++) {
819 (void)srn_fiber_spawn(ctx, fiber_pending_entry, nullptr);
820 }
821
822 // Returns when the stopper calls srn_sched_stop, with the pending fibers
823 // still queued.
824 srn_sched_run(sched, 1);
825
826 // Only the stopper ran (three rounds); no pending fiber ran.
827 TEST_CHECK(atomic_load(&fiber_stop_rounds) == 3);
828
829 // Reap the pending fibers while their structs (in the context block) are
830 // still alive, before releasing the context. SHUTDOWN_ENGINE calls shutdown
831 // again, which is a no-op on an already torn-down scheduler.
832 srn_sched_shutdown(sched);
833
834 RELEASE_CONTEXT(ctx);
835 SHUTDOWN_ENGINE(mm, engine);
836}
#define ROUNDS
#define TEST_CHECK(cond)
Definition acutest.h:95
#define TEST_ASSERT(cond)
Definition acutest.h:117
va_list args
Definition acutest.h:876
#define TEST_MSG(...)
Definition acutest.h:223
#define RELEASE_CONTEXT(x)
Definition base.h:48
#define ASSERT_NOT_NULL(x)
Definition base.h:30
#define SHUTDOWN_ENGINE(mm, engine)
Definition base.h:40
#define MAKE_ENGINE(mm, engine)
Definition base.h:34
#define MAKE_CONTEXT(engine, x)
Definition base.h:44
#define SRN_CONFIG_DEFAULT_FIBER_STACK_SIZE
Size of every fiber stack, in bytes.
#define SRN_CONFIG_DEFAULT_FIBER_GUARD_PAGES
Pages in the guard band below every fiber stack.
size_t srn_mm_get_os_page_size(void)
Retutrns the OS page size.
Definition default.c:312
void srn_fiber_switch_final(srn_fiber_t *to)
Like srn_fiber_switch, but for a fiber that has finished and must not be resumed, control transfers t...
Definition fiber.c:87
srn_fiber_t * srn_fiber_spawn(srn_context_t *ctx, srn_fiber_entry_t entry, void *arg)
Make and schedule a fiber with every default, the engine's scheduler, the configured stack size,...
Definition fiber.c:250
void srn_fiber_init_thread(srn_fiber_t *f)
Represent the calling OS thread as the running fiber ("#0"), so the scheduler or a test can switch aw...
Definition fiber.c:153
void srn_fiber_switch(srn_fiber_t *from, srn_fiber_t *to)
Compiled without AddressSanitizer instrumentation, in stack-use-after-return mode ASan would place fr...
Definition fiber.c:65
srn_fiber_t * srn_fiber_make(srn_context_t *ctx, srn_scheduler_t *sched, const char *name, srn_fiber_entry_t entry, void *arg, size_t stack_size)
Create a fiber that will run entry(ctx, arg), registered with sched but NOT scheduled.
Definition fiber.c:202
void srn_fiber_on_entry(srn_fiber_t *from)
Call as the first action inside a fresh fiber's entry.
Definition fiber.c:110
AI Generated (🤦) Fiber subsystem overview.
srn_fiber_stack_t srn_fiber_stack_alloc(size_t size, size_t guard_pages)
Allocate a stack of at least size usable bytes, or SRN_CONFIG_DEFAULT_FIBER_STACK_SIZE when size is 0...
static size_t srn_fiber_stack_size(srn_fiber_stack_t s)
Definition fiber.h:612
void srn_fiber_ctx_make(srn_fiber_ctx_t *fiber_ctx, srn_fiber_stack_t stack, void(*fn)(void *), void *arg)
Initialise a fresh fiber context so the first srn_fiber_swap into it begins executing fn(arg) on stac...
#define SRN_FIBER_SPAWN_COPY(ctx, entry, value)
srn_fiber_spawn_copy for an lvalue, the address and size are taken for the caller.
Definition fiber.h:459
void srn_fiber_stack_free(srn_fiber_stack_t stack)
@ SRN_FIBER_NEW
Created, stack mapped, never resumed.
Definition fiber.h:221
@ SRN_FIBER_RUNNING
Currently executing.
Definition fiber.h:225
@ SRN_FIBER_READY
On the run queue, eligible to run.
Definition fiber.h:223
@ SRN_FIBER_DONE
Entry returned. The result is final.
Definition fiber.h:229
@ SRN_FIBER_SUSPENDED
Parked off the run queue, awaits srn_fiber_ready.
Definition fiber.h:227
void * srn_fiber_result_t
What a fiber's entry produces, type-erased.
Definition fiber.h:161
static int fiber_wf_woke
static int fiber_yield_n
static srn_fiber_t * fiber_dwake_waiter
static void test_fiber_sched_registry()
static void test_fiber_sched_wait_for_many()
static bool fiber_dwake_signalled
static void test_fiber_switch()
static int fiber_wf_result
static srn_fiber_result_t auto_workers_entry(srn_context_t *ctx, void *arg)
static atomic_int fiber_mt_yield_counter
static srn_fiber_result_t fiber_pending_entry(srn_context_t *ctx, void *arg)
static void test_fiber_sched_double_wake()
static srn_fiber_result_t fiber_run_entry(srn_context_t *ctx, void *arg)
static bool fiber_mbox_park(srn_fiber_t *self, void *arg)
static srn_fiber_result_t fiber_wf_seen
static void test_fiber_make_schedule()
static atomic_int fiber_mt_counter
static srn_fiber_result_t fiber_dwake_consumer_entry(srn_context_t *ctx, void *arg)
static srn_fiber_result_t fiber_yield_entry(srn_context_t *ctx, void *arg)
static void test_fiber_sched_mt_yield()
static void test_fiber_sched_auto_workers()
static srn_fiber_t * auto_name_child
Definition fiber_tests.h:78
static int fiber_rr_n
static srn_fiber_result_t auto_name_parent_entry(srn_context_t *ctx, void *arg)
Definition fiber_tests.h:80
static void test_fiber_stack()
static srn_fiber_result_t spawn_copy_entry(srn_context_t *ctx, void *arg)
static srn_fiber_result_t fiber_mt_entry(srn_context_t *ctx, void *arg)
static srn_fiber_result_t fiber_stopper_entry(srn_context_t *ctx, void *arg)
static srn_fiber_result_t fiber_wf_target_entry(srn_context_t *ctx, void *arg)
static int spawn_copy_seen
static void test_fiber_thread_stack_bounds()
Definition fiber_tests.h:56
static void test_fiber_types()
static int fiber_yield_log[4]
static srn_fiber_t fiber_pp_thread
static void test_fiber_spawn_copy()
static srn_fiber_result_t fiber_rr_entry(srn_context_t *ctx, void *arg)
static void test_fiber_sched_mt_run()
static int fiber_run_counter
static void test_fiber_sched_yield_round_robin()
static srn_fiber_result_t fiber_dwake_producer_entry(srn_context_t *ctx, void *arg)
static int fiber_dwake_runs
static srn_fiber_result_t fiber_wf_counting_waiter(srn_context_t *ctx, void *arg)
static atomic_int auto_workers_ran
static void test_fiber_sched_suspend()
static bool fiber_stuck_park(srn_fiber_t *self, void *arg)
static void test_fiber_sched_park_abort()
static srn_fiber_result_t fiber_stuck_entry(srn_context_t *ctx, void *arg)
static atomic_int fiber_stop_rounds
static int fiber_rr_log[6]
static srn_fiber_result_t auto_name_entry(srn_context_t *ctx, void *arg)
Definition fiber_tests.h:72
static srn_fiber_result_t fiber_producer_entry(srn_context_t *ctx, void *arg)
static void test_fiber_auto_name()
Definition fiber_tests.h:90
static srn_fiber_result_t fiber_mt_yield_entry(srn_context_t *ctx, void *arg)
static void test_fiber_sched_yield()
static void test_fiber_sched_wait_for()
static bool fiber_dwake_park(srn_fiber_t *self, void *arg)
static void test_fiber_sched_run()
static int fiber_pp_rounds
static void fiber_pp_entry(void *arg)
static srn_fiber_result_t fiber_wf_waiter_entry(srn_context_t *ctx, void *arg)
static srn_fiber_t * fiber_wf_target
static void test_fiber_sched_stop()
static int fiber_ok_value
static srn_fiber_t fiber_pp_worker
static int fiber_pp_ticks
static int fiber_mbox_got
static fiber_mbox_t fiber_mbox
static srn_fiber_result_t fiber_consumer_entry(srn_context_t *ctx, void *arg)
static void test_fiber_sched_wait_for_done()
void srn_fiber_ready(srn_fiber_t *fiber)
Mark a suspended fiber runnable again, waking it when the event it awaited occurs.
Definition scheduler.c:1083
srn_fiber_result_t srn_fiber_wait_for(srn_fiber_t *target)
Block the calling fiber until target finishes, then return its result.
Definition scheduler.c:1130
void srn_sched_shutdown(srn_scheduler_t *sched)
The one stop tear down of the fiber subsystem, should be called once srn_sched_run has returned.
Definition scheduler.c:333
void srn_sched_stop(srn_scheduler_t *sched)
Ask a running scheduler to stop.
Definition scheduler.c:978
void srn_fiber_schedule(srn_fiber_t *fiber)
Schedule a NEW fiber, making it eligible to run.
Definition scheduler.c:638
void srn_sched_run(srn_scheduler_t *sched, size_t nworkers)
Run the scheduler with nworkers os threads draining it, returning once the pool goes quiescent (every...
Definition scheduler.c:875
void srn_fiber_suspend(srn_fiber_park_fn commit, void *arg)
A suspended fiber is on no scheduler queue, and the scheduler does not track what it waits on – whoev...
Definition scheduler.c:1063
void srn_fiber_yield(void)
Yield cooperatively, re-enqueue the running fiber and run the next ready one.
Definition scheduler.c:1039
srn_fiber_t * waiter
The saved context of a suspended fiber is a single word, its stack pointer at the moment it was switc...
Definition fiber.h:187
One stack per fiber, mapped with a guard band at the low end so an overflow faults deterministically ...
Definition fiber.h:207
void * guard
Low base of the protected guard band that detects stack overflows.
Definition fiber.h:213
void * limit
Low end of usable region.
Definition fiber.h:211
void * start
High end, stack pointer initialises to this address.
Definition fiber.h:209
char name[SRN_FIBER_NAME_MAX]
Debug name, the caller's choice copied at creation, or autogenerated when the caller passed none (see...
Definition fiber.h:318
_Atomic srn_fiber_state_t state
The lifecycle state.
Definition fiber.h:262
srn_fiber_stack_t stack
Definition fiber.h:256
srn_thread_t, srn_mutex_t, and srn_cond_t model the thread-level operations the runtime needs,...
size_t srn_thread_cpu_count(void)
The number of CPUs the calling process may run threads on, at least 1.
#define UNUSED(x)
Definition utils.h:45