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 <[email protected]>
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
228
229 TEST_CHECK(s.guard != nullptr);
230 TEST_CHECK(s.limit != nullptr);
231 TEST_CHECK(s.start != nullptr);
232 // The guard sits exactly one page below the usable region.
233 TEST_CHECK((char *)s.limit == (char *)s.guard + page);
234 // The usable region is non-empty, page-aligned, and at least the default.
235 TEST_CHECK((char *)s.start > (char *)s.limit);
236 TEST_CHECK(((uintptr_t)s.limit & (page - 1)) == 0);
238
239 // The whole usable region is writable end to end. The guard is left alone.
240 ((char *)s.limit)[0] = (char)0x5A;
241 ((char *)s.start)[-1] = (char)0x5A;
242
244}
245
246// Ping-pong, a worker fiber and the calling thread hand control back and forth.
247// This exercises the whole switch path -- srn_fiber_ctx_make seeds the worker,
248// the first srn_fiber_switch starts it via the trampoline, each yield resumes
249// the thread, and the worker finishes with srn_fiber_switch_final. The worker
250// runs on a real guard-paged stack from the provider.
253static int fiber_pp_ticks;
255
256static void fiber_pp_entry(void *arg) {
257 UNUSED(arg);
259 for (int i = 0; i < fiber_pp_rounds; i++) {
262 }
264}
265
266static void test_fiber_switch() {
268
270
272 fiber_pp_worker.stack = worker_stack;
274 srn_fiber_ctx_make(&fiber_pp_worker.fiber_ctx, worker_stack, fiber_pp_entry, nullptr);
275
276 fiber_pp_ticks = 0;
277 fiber_pp_rounds = 5;
278
279 // `rounds` yields, plus one more switch to let the worker run to completion.
280 for (int i = 0; i <= fiber_pp_rounds; i++) {
282 }
283
286
287 srn_fiber_stack_free(worker_stack);
288}
289
290// A shared return value, these tests do not inspect the payload, only that a
291// fiber ran and finished (a null result is legal too).
292static int fiber_ok_value;
293
294// fiber::sched::run -- spawn N fibers, each bumps a counter and returns. After
295// srn_sched_run drains the queue every fiber must have run once and reached
296// DONE, with its result recorded.
298
300 UNUSED(ctx);
301 UNUSED(arg);
303 return &fiber_ok_value;
304}
305
306static void test_fiber_sched_run() {
307 MAKE_ENGINE(mm, engine);
308 MAKE_CONTEXT(engine, ctx);
309 srn_scheduler_t *sched = engine->scheduler;
310 ASSERT_NOT_NULL(sched);
311
313 enum { N = 4 };
314 srn_fiber_t *fibers[N];
315 for (int i = 0; i < N; i++) {
316 fibers[i] = srn_fiber_spawn(ctx, fiber_run_entry, nullptr);
317 ASSERT_NOT_NULL(fibers[i]);
318 }
319
320 srn_sched_run(sched, 1);
321
323 for (int i = 0; i < N; i++) {
324 // The struct lives in the context block after reaping. Only the stack is
325 // released, so state and result are still readable here.
326 TEST_CHECK(fibers[i]->state == SRN_FIBER_DONE);
327 TEST_CHECK(fibers[i]->result == &fiber_ok_value);
328 }
329
330 RELEASE_CONTEXT(ctx);
331 SHUTDOWN_ENGINE(mm, engine);
332}
333
334// fiber::sched::yield -- two fibers each log their id twice, yielding between.
335// Run on one worker. Per-worker queues do not guarantee a strict interleaving,
336// so the invariant checked is that both fibers ran to completion, each id
337// appears exactly twice, four entries in all.
338static int fiber_yield_log[4];
339static int fiber_yield_n;
340
342 UNUSED(ctx);
343 int id = (int)(intptr_t)arg;
344 for (int i = 0; i < 2; i++) {
347 }
348 return &fiber_ok_value;
349}
350
352 MAKE_ENGINE(mm, engine);
353 MAKE_CONTEXT(engine, ctx);
354 srn_scheduler_t *sched = engine->scheduler;
355 ASSERT_NOT_NULL(sched);
356
357 fiber_yield_n = 0;
358 (void)srn_fiber_spawn(ctx, fiber_yield_entry, (void *)(intptr_t)1);
359 (void)srn_fiber_spawn(ctx, fiber_yield_entry, (void *)(intptr_t)2);
360
361 srn_sched_run(sched, 1);
362
364 int ones = 0;
365 int twos = 0;
366 for (int i = 0; i < 4; i++) {
367 if (fiber_yield_log[i] == 1) {
368 ones++;
369 } else if (fiber_yield_log[i] == 2) {
370 twos++;
371 }
372 }
373 TEST_CHECK(ones == 2);
374 TEST_CHECK(twos == 2);
375
376 RELEASE_CONTEXT(ctx);
377 SHUTDOWN_ENGINE(mm, engine);
378}
379
380// fiber::sched::yield_round_robin -- yield moves the fiber to the back of the
381// queue, so two yielding fibers on one worker interleave step by step. A
382// yield that re-runs its own fiber produces runs of the same id instead.
383static int fiber_rr_log[6];
384static int fiber_rr_n;
385
387 UNUSED(ctx);
388 int id = (int)(intptr_t)arg;
389 for (int i = 0; i < 3; i++) {
390 if (fiber_rr_n < 6) {
391 fiber_rr_log[fiber_rr_n++] = id;
392 }
394 }
395 return &fiber_ok_value;
396}
397
399 MAKE_ENGINE(mm, engine);
400 MAKE_CONTEXT(engine, ctx);
401 srn_scheduler_t *sched = engine->scheduler;
402 ASSERT_NOT_NULL(sched);
403
404 fiber_rr_n = 0;
405 (void)srn_fiber_spawn(ctx, fiber_rr_entry, (void *)(intptr_t)1);
406 (void)srn_fiber_spawn(ctx, fiber_rr_entry, (void *)(intptr_t)2);
407
408 srn_sched_run(sched, 1);
409
411 // Strict alternation, a yielding fiber never runs two steps back to back
412 // while a peer is ready.
413 for (int i = 1; i < 6; i++) {
415 TEST_MSG(
416 "order: %d %d %d %d %d %d", fiber_rr_log[0], fiber_rr_log[1], fiber_rr_log[2],
418 );
419 }
420
421 RELEASE_CONTEXT(ctx);
422 SHUTDOWN_ENGINE(mm, engine);
423}
424
425// fiber::sched::suspend -- a one-slot mailbox handoff. The consumer runs first
426// and parks. Its commit registers it as the waiter (the slot is empty). The
427// producer fills the slot and readies the consumer, which resumes and reads it.
428typedef struct {
429 int value;
433
435static int fiber_mbox_got;
436
437// Park commit, register as the mailbox waiter, unless a value is already there
438// (in which case decline to park so the consumer resumes at once).
439static bool fiber_mbox_park(srn_fiber_t *self, void *arg) {
440 fiber_mbox_t *mb = arg;
441 if (mb->has_value) {
442 return false;
443 }
444 mb->waiter = self;
445 return true;
446}
447
455
457 UNUSED(ctx);
458 UNUSED(arg);
459 fiber_mbox.value = 42;
460 fiber_mbox.has_value = true;
461 if (fiber_mbox.waiter != nullptr) {
462 srn_fiber_t *w = fiber_mbox.waiter;
463 fiber_mbox.waiter = nullptr;
465 }
466 return &fiber_ok_value;
467}
468
470 MAKE_ENGINE(mm, engine);
471 MAKE_CONTEXT(engine, ctx);
472 srn_scheduler_t *sched = engine->scheduler;
473 ASSERT_NOT_NULL(sched);
474
476 fiber_mbox_got = 0;
477
478 // Order matters, the consumer must be enqueued first so it runs and suspends
479 // before the producer fills the slot.
480 (void)srn_fiber_spawn(ctx, fiber_consumer_entry, nullptr);
481 (void)srn_fiber_spawn(ctx, fiber_producer_entry, nullptr);
482
483 srn_sched_run(sched, 1);
484
486
487 RELEASE_CONTEXT(ctx);
488 SHUTDOWN_ENGINE(mm, engine);
489}
490
491// fiber::sched::park_abort -- the commit declines to park. The value is already
492// in the slot when the consumer runs, so fiber_mbox_park returns false and the
493// consumer resumes immediately without ever parking or registering a waiter.
495 MAKE_ENGINE(mm, engine);
496 MAKE_CONTEXT(engine, ctx);
497 srn_scheduler_t *sched = engine->scheduler;
498 ASSERT_NOT_NULL(sched);
499
501 fiber_mbox.value = 7;
502 fiber_mbox.has_value = true; // value present before the consumer runs
503 fiber_mbox_got = 0;
504
505 (void)srn_fiber_spawn(ctx, fiber_consumer_entry, nullptr);
506
507 srn_sched_run(sched, 1);
508
510 TEST_CHECK(fiber_mbox.waiter == nullptr); // never registered as a waiter
511
512 RELEASE_CONTEXT(ctx);
513 SHUTDOWN_ENGINE(mm, engine);
514}
515
516// fiber::sched::registry -- a fiber that suspends with no one to wake it. The
517// run loop drains the ready queue and returns, leaving the fiber parked and its
518// stack mapped. The scheduler still tracks it through the registry, so shutdown
519// reclaims the stack instead of leaking it.
520// Park commit that registers the fiber nowhere, so nothing can ever wake it.
521static bool fiber_stuck_park(srn_fiber_t *self, void *arg) {
522 UNUSED(self);
523 UNUSED(arg);
524 return true; // stay parked, with no waker
525}
526
528 UNUSED(ctx);
529 UNUSED(arg);
530 srn_fiber_suspend(fiber_stuck_park, nullptr); // never readied
531 return &fiber_ok_value;
532}
533
535 MAKE_ENGINE(mm, engine);
536 MAKE_CONTEXT(engine, ctx);
537 srn_scheduler_t *sched = engine->scheduler;
538 ASSERT_NOT_NULL(sched);
539
540 srn_fiber_t *stuck = srn_fiber_spawn(ctx, fiber_stuck_entry, nullptr);
541 ASSERT_NOT_NULL(stuck);
542
543 // Drains the ready queue. The stuck fiber suspends and is abandoned.
544 srn_sched_run(sched, 1);
546
547 // The registry lets shutdown find and free the abandoned fiber's stack.
548 srn_sched_shutdown(sched);
549
550 RELEASE_CONTEXT(ctx);
551 SHUTDOWN_ENGINE(mm, engine);
552}
553
554// fiber::sched::double_wake -- two wakers ready the same suspended fiber (as an
555// IO event and a timeout might race). The wake must be idempotent, the fiber
556// runs exactly once, not enqueued twice. A double enqueue would resume an
557// already-reaped fiber on the second pass and trip the sanitizer.
561
562static bool fiber_dwake_park(srn_fiber_t *self, void *arg) {
563 UNUSED(arg);
565 return false;
566 }
567 fiber_dwake_waiter = self;
568 return true;
569}
570
572 UNUSED(ctx);
573 UNUSED(arg);
576 return &fiber_ok_value;
577}
578
580 UNUSED(ctx);
581 UNUSED(arg);
583 if (fiber_dwake_waiter != nullptr) {
585 fiber_dwake_waiter = nullptr;
586 srn_fiber_ready(w); // first waker: SUSPENDED -> READY, enqueued
587 srn_fiber_ready(w); // second waker: already READY -> no-op
588 }
589 return &fiber_ok_value;
590}
591
593 MAKE_ENGINE(mm, engine);
594 MAKE_CONTEXT(engine, ctx);
595 srn_scheduler_t *sched = engine->scheduler;
596 ASSERT_NOT_NULL(sched);
597
598 fiber_dwake_waiter = nullptr;
599 fiber_dwake_signalled = false;
601
602 // Consumer first, so it runs and suspends before the producer wakes it.
603 (void)srn_fiber_spawn(ctx, fiber_dwake_consumer_entry, nullptr);
604 (void)srn_fiber_spawn(ctx, fiber_dwake_producer_entry, nullptr);
605
606 srn_sched_run(sched, 1);
607
609
610 RELEASE_CONTEXT(ctx);
611 SHUTDOWN_ENGINE(mm, engine);
612}
613
614// fiber::sched::wait_for -- the caller blocks until the target finishes and
615// reads its result. The waiter runs first and parks. The target runs later,
616// finishes, and the DONE handling wakes the waiter.
617static int fiber_wf_result; // the target's result
618static srn_fiber_t *fiber_wf_target; // set before the run
619static srn_fiber_result_t fiber_wf_seen; // what the waiter read
620
622 UNUSED(ctx);
623 UNUSED(arg);
624 return &fiber_wf_result;
625}
626
633
635 MAKE_ENGINE(mm, engine);
636 MAKE_CONTEXT(engine, ctx);
637 srn_scheduler_t *sched = engine->scheduler;
638 ASSERT_NOT_NULL(sched);
639
640 fiber_wf_seen = nullptr;
641 // Waiter made first, so it runs and parks before the target finishes.
642 (void)srn_fiber_spawn(ctx, fiber_wf_waiter_entry, nullptr);
644
645 srn_sched_run(sched, 1);
646
648
649 RELEASE_CONTEXT(ctx);
650 SHUTDOWN_ENGINE(mm, engine);
651}
652
653// fiber::sched::wait_for_done -- waiting for an already-finished target returns
654// its result at once. The commit sees DONE and declines to park.
656 MAKE_ENGINE(mm, engine);
657 MAKE_CONTEXT(engine, ctx);
658 srn_scheduler_t *sched = engine->scheduler;
659 ASSERT_NOT_NULL(sched);
660
661 fiber_wf_seen = nullptr;
662 // Target made first, so it runs and finishes before the waiter runs.
664 (void)srn_fiber_spawn(ctx, fiber_wf_waiter_entry, nullptr);
665
666 srn_sched_run(sched, 1);
667
669
670 RELEASE_CONTEXT(ctx);
671 SHUTDOWN_ENGINE(mm, engine);
672}
673
674// fiber::sched::wait_for_many -- several fibers wait for one target. All of
675// them wake and read its result once it finishes.
676static int fiber_wf_woke;
677
679 UNUSED(ctx);
680 UNUSED(arg);
683 }
684 return &fiber_ok_value;
685}
686
688 MAKE_ENGINE(mm, engine);
689 MAKE_CONTEXT(engine, ctx);
690 srn_scheduler_t *sched = engine->scheduler;
691 ASSERT_NOT_NULL(sched);
692
693 fiber_wf_woke = 0;
694 // Three waiters made first so they all park, then the target finishes.
695 (void)srn_fiber_spawn(ctx, fiber_wf_counting_waiter, nullptr);
696 (void)srn_fiber_spawn(ctx, fiber_wf_counting_waiter, nullptr);
697 (void)srn_fiber_spawn(ctx, fiber_wf_counting_waiter, nullptr);
699
700 srn_sched_run(sched, 1);
701
703
704 RELEASE_CONTEXT(ctx);
705 SHUTDOWN_ENGINE(mm, engine);
706}
707
708// fiber::sched::mt_run -- many fibers across several worker threads, each doing
709// one atomic increment. With true parallelism the only safe shared state is the
710// atomic counter. After the run every increment must have landed (no lost
711// updates) and the pool must have reached quiescence, which is what makes
712// srn_sched_run return.
713static atomic_int fiber_mt_counter;
714
716 UNUSED(ctx);
717 UNUSED(arg);
718 atomic_fetch_add(&fiber_mt_counter, 1);
719 return &fiber_ok_value;
720}
721
723 MAKE_ENGINE(mm, engine);
724 MAKE_CONTEXT(engine, ctx);
725 srn_scheduler_t *sched = engine->scheduler;
726 ASSERT_NOT_NULL(sched);
727
728 atomic_store(&fiber_mt_counter, 0);
729 enum { N = 256 };
730 for (int i = 0; i < N; i++) {
731 (void)srn_fiber_spawn(ctx, fiber_mt_entry, nullptr);
732 }
733
734 srn_sched_run(sched, 4);
735
736 TEST_CHECK(atomic_load(&fiber_mt_counter) == N);
737
738 RELEASE_CONTEXT(ctx);
739 SHUTDOWN_ENGINE(mm, engine);
740}
741
742// fiber::sched::mt_yield -- fibers that yield repeatedly across worker threads,
743// so each is re-enqueued between rounds and may resume on a different thread
744// than it last ran on. Exercises the cross-thread re-enqueue and the park/wake
745// path under contention. The counter totals every round of every fiber.
746static atomic_int fiber_mt_yield_counter;
747
749 UNUSED(ctx);
750 int rounds = (int)(intptr_t)arg;
751 for (int i = 0; i < rounds; i++) {
752 atomic_fetch_add(&fiber_mt_yield_counter, 1);
754 }
755 return &fiber_ok_value;
756}
757
759 MAKE_ENGINE(mm, engine);
760 MAKE_CONTEXT(engine, ctx);
761 srn_scheduler_t *sched = engine->scheduler;
762 ASSERT_NOT_NULL(sched);
763
764 atomic_store(&fiber_mt_yield_counter, 0);
765 enum { N = 64, ROUNDS = 8 };
766 for (int i = 0; i < N; i++) {
767 (void)srn_fiber_spawn(ctx, fiber_mt_yield_entry, (void *)(intptr_t)ROUNDS);
768 }
769
770 srn_sched_run(sched, 4);
771
772 TEST_CHECK(atomic_load(&fiber_mt_yield_counter) == N * ROUNDS);
773
774 RELEASE_CONTEXT(ctx);
775 SHUTDOWN_ENGINE(mm, engine);
776}
777
778// fiber::sched::stop -- a fiber stops the scheduler before all the work is
779// done. The stopper runs a few rounds then calls srn_sched_stop, so
780// srn_sched_run returns instead of draining the queue. The pending fibers
781// behind it are left unrun, and srn_sched_shutdown reaps their stacks. Checks
782// the run returns rather than hangs, only the stopper ran, and the leftover
783// teardown is clean.
784static atomic_int fiber_stop_rounds;
785
787 UNUSED(ctx);
788 srn_scheduler_t *sched = arg;
789 // No yield in this loop, a yielded fiber goes to the back of the queue, so
790 // yielding would hand the pending fibers a turn before the stop lands.
791 for (int i = 0; i < 3; i++) {
792 atomic_fetch_add(&fiber_stop_rounds, 1);
793 }
794 srn_sched_stop(sched);
795 return &fiber_ok_value;
796}
797
799 UNUSED(ctx);
800 UNUSED(arg);
801 // Never reached, the stopper ends the run first. It adds a large amount, so
802 // any accidental execution is visible in the count.
803 atomic_fetch_add(&fiber_stop_rounds, 1000);
804 return &fiber_ok_value;
805}
806
808 MAKE_ENGINE(mm, engine);
809 MAKE_CONTEXT(engine, ctx);
810 srn_scheduler_t *sched = engine->scheduler;
811 ASSERT_NOT_NULL(sched);
812
813 atomic_store(&fiber_stop_rounds, 0);
814 // Stopper first so it reaches the single worker before the pending fibers.
815 (void)srn_fiber_spawn(ctx, fiber_stopper_entry, sched);
816 enum { PENDING = 3 };
817 for (int i = 0; i < PENDING; i++) {
818 (void)srn_fiber_spawn(ctx, fiber_pending_entry, nullptr);
819 }
820
821 // Returns when the stopper calls srn_sched_stop, with the pending fibers
822 // still queued.
823 srn_sched_run(sched, 1);
824
825 // Only the stopper ran (three rounds); no pending fiber ran.
826 TEST_CHECK(atomic_load(&fiber_stop_rounds) == 3);
827
828 // Reap the pending fibers while their structs (in the context block) are
829 // still alive, before releasing the context. SHUTDOWN_ENGINE calls shutdown
830 // again, which is a no-op on an already torn-down scheduler.
831 srn_sched_shutdown(sched);
832
833 RELEASE_CONTEXT(ctx);
834 SHUTDOWN_ENGINE(mm, engine);
835}
#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:46
#define ASSERT_NOT_NULL(x)
Definition base.h:30
#define SHUTDOWN_ENGINE(mm, engine)
Definition base.h:38
#define MAKE_ENGINE(mm, engine)
Definition base.h:32
#define MAKE_CONTEXT(engine, x)
Definition base.h:42
size_t srn_mm_get_os_page_size(void)
Retutrns the OS page size.
Definition default.c:313
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:245
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:200
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)
Allocate a stack of at least size usable bytes plus a guard page, or SRN_FIBER_DEFAULT_STACK_SIZE whe...
static size_t srn_fiber_stack_size(srn_fiber_stack_t s)
Definition fiber.h:625
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:474
void srn_fiber_stack_free(srn_fiber_stack_t stack)
@ SRN_FIBER_NEW
Created, stack mapped, never resumed.
Definition fiber.h:236
@ SRN_FIBER_RUNNING
Currently executing.
Definition fiber.h:240
@ SRN_FIBER_READY
On the run queue, eligible to run.
Definition fiber.h:238
@ SRN_FIBER_DONE
Entry returned. The result is final.
Definition fiber.h:244
@ SRN_FIBER_SUSPENDED
Parked off the run queue, awaits srn_fiber_ready.
Definition fiber.h:242
#define SRN_FIBER_DEFAULT_STACK_SIZE
Default size of every fiber stack.
Definition fiber.h:184
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:202
One stack per fiber, mapped with a guard page at the low end so an overflow faults deterministically ...
Definition fiber.h:222
void * guard
The protected page to detect stack overflows.
Definition fiber.h:228
void * limit
Low end of usable region.
Definition fiber.h:226
void * start
High end, stack pointer initialises to this address.
Definition fiber.h:224
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:333
_Atomic srn_fiber_state_t state
The lifecycle state.
Definition fiber.h:277
srn_fiber_stack_t stack
Definition fiber.h:271
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