| 1 | /* @title: Test worker fleet */ |
| 2 | #pragma once |
| 3 | #include <compiler/core.h> |
| 4 | #include <compiler/wrapper.h> |
| 5 | #include <stdbool.h> |
| 6 | #include <stddef.h> |
| 7 | #include <stdint.h> |
| 8 | #include <sync/once_token.h> |
| 9 | #include <test/conc.h> |
| 10 | #include <test/perturb.h> |
| 11 | #include <test/test.h> |
| 12 | #include <time/time.h> |
| 13 | #include <time/timer.h> |
| 14 | |
| 15 | struct test_fleet; |
| 16 | struct test_fleet_alloc; |
| 17 | |
| 18 | /* Worker thread fleets |
| 19 | * |
| 20 | * The idea here: we have a pointer to one of these in struct test_context. |
| 21 | * |
| 22 | * When a test needs a fleet, it simply test_fleet_init()'s the ctx |
| 23 | * fleet, and the runner will handle cleanup. |
| 24 | * |
| 25 | * NOTE: Tests must not free their fleets, and |
| 26 | * |
| 27 | * TODO: Add mechanisms to track the running context (test, harness, |
| 28 | * etc.) so in the future, we can detect and panic if a test |
| 29 | * tries to free its fleet |
| 30 | */ |
| 31 | |
| 32 | /* Options for spawning individual workers in a fleet */ |
| 33 | struct test_fleet_worker_opts { |
| 34 | bool pinned; |
| 35 | void *arg; |
| 36 | cpu_id_t core_id; /* CPU_ID_MAX = unpinned */ |
| 37 | enum thread_prio_class prio_class; |
| 38 | enum thread_flags flags; |
| 39 | }; |
| 40 | |
| 41 | /* TODO: Bump/scale around with CPU count and whatnot */ |
| 42 | #define TEST_FLEET_MAX_WORKERS 128 |
| 43 | |
| 44 | #define TEST_FLEET_SOFT_MS 30000 |
| 45 | |
| 46 | /* Give some leeway for the workers to calm down */ |
| 47 | #define TEST_FLEET_DRAIN_MS 10000 |
| 48 | |
| 49 | struct test_fleet_opts { |
| 50 | /* 0 falls back to defaults */ |
| 51 | time_ms_t soft_ms; |
| 52 | time_ms_t drain_ms; |
| 53 | |
| 54 | /* Workers run until they get stopped */ |
| 55 | bool run_until_stop; |
| 56 | |
| 57 | /* Pin on whatever CPU the scheduler |
| 58 | * decides it wants to put the thread on, |
| 59 | * which can help increase contention and stop |
| 60 | * the balancer from distributing threads */ |
| 61 | bool pin; |
| 62 | }; |
| 63 | |
| 64 | /* Worker threads return false to say "I tripped some assertion", |
| 65 | * and the outer loop captures that */ |
| 66 | typedef bool (*test_worker_fn)(struct test_fleet *f, |
| 67 | struct test_conc_worker *self); |
| 68 | |
| 69 | struct test_fleet { |
| 70 | struct test_context *ctx; |
| 71 | struct test_conc conc; |
| 72 | struct test_conc_worker workers[TEST_FLEET_MAX_WORKERS]; |
| 73 | test_worker_fn bodies[TEST_FLEET_MAX_WORKERS]; |
| 74 | size_t count; |
| 75 | |
| 76 | struct test_fleet_opts opts; |
| 77 | |
| 78 | /* |
| 79 | * This is a wrapper to prevent tests from manually managing their |
| 80 | * memory. It's the head of a SLIST of nodes, and at |
| 81 | * test exit, we iterate over it and free everything |
| 82 | * |
| 83 | * TODO: We really need to make such abstractions a global |
| 84 | * primitive to embed allocators instead of making them ad-hoc. |
| 85 | * |
| 86 | */ |
| 87 | struct test_fleet_alloc *allocs; |
| 88 | |
| 89 | /* workers[0, subject_count) are subjects, and the rest are perturbers */ |
| 90 | size_t subject_count; |
| 91 | bool has_perturbers; |
| 92 | struct test_perturb_env perturb_env; |
| 93 | |
| 94 | /* Whoever fails first, gets it first */ |
| 95 | ONCE_TOKEN_DEFINE(bool, failed); |
| 96 | const char *fail_file; |
| 97 | uint32_t fail_line; |
| 98 | |
| 99 | atomic_size_t started; |
| 100 | atomic_size_t finished; |
| 101 | |
| 102 | struct test_liveness_state liveness; |
| 103 | struct timer hard_timer; |
| 104 | bool hard_armed; |
| 105 | bool joined; |
| 106 | bool spawn_failed; /* NOTE: this seems odd to even handle, but down |
| 107 | * the line, say, when kernel tests are runnable |
| 108 | * from userspace, we can't just panic if |
| 109 | * an allocation fails (unfortunate) */ |
| 110 | }; |
| 111 | |
| 112 | struct test_fleet *test_fleet_init_full(struct test_context *ctx, |
| 113 | const struct test_fleet_opts opts); |
| 114 | |
| 115 | #define test_fleet_opts_with_defaults(...) \ |
| 116 | cc_wno_override_init_expr( \ |
| 117 | struct test_fleet_opts, \ |
| 118 | ((struct test_fleet_opts) {.soft_ms = TEST_FLEET_SOFT_MS, \ |
| 119 | .drain_ms = TEST_FLEET_DRAIN_MS, \ |
| 120 | ##__VA_ARGS__})) |
| 121 | |
| 122 | #define test_fleet_init(ctx, ...) \ |
| 123 | test_fleet_init_full((ctx), test_fleet_opts_with_defaults(__VA_ARGS__)) |
| 124 | |
| 125 | void *test_fleet_alloc(struct test_fleet *f, size_t size) cw_alloc(2); |
| 126 | |
| 127 | /* Spawn with custom worker options (affinity, priority class, flags) */ |
| 128 | struct test_conc_worker * |
| 129 | test_fleet_spawn_full(struct test_fleet *f, const char *role, |
| 130 | test_worker_fn body, |
| 131 | const struct test_fleet_worker_opts opts); |
| 132 | |
| 133 | #define test_fleet_worker_opts_with_defaults(...) \ |
| 134 | cc_wno_override_init_expr(struct test_fleet_worker_opts, \ |
| 135 | ((struct test_fleet_worker_opts) { \ |
| 136 | .core_id = CPU_ID_MAX, \ |
| 137 | .prio_class = THREAD_PRIO_CLASS_TIMESHARE, \ |
| 138 | .pinned = false, \ |
| 139 | ##__VA_ARGS__})) |
| 140 | |
| 141 | #define test_fleet_spawn(f, role, body, ...) \ |
| 142 | test_fleet_spawn_full((f), (role), (body), \ |
| 143 | test_fleet_worker_opts_with_defaults(__VA_ARGS__)) |
| 144 | |
| 145 | /* Look up a worker thread by its role name */ |
| 146 | struct thread *test_fleet_worker_thread(struct test_fleet *f, const char *role); |
| 147 | |
| 148 | /* Spawn N many per CPU */ |
| 149 | size_t test_fleet_spawn_per_core(struct test_fleet *f, const char *role, |
| 150 | test_worker_fn body, void *arg, |
| 151 | size_t per_core); |
| 152 | |
| 153 | /* Background noise against every worker, one perturber per name */ |
| 154 | size_t test_fleet_spawn_perturbers(struct test_fleet *f, |
| 155 | const char *const *names, |
| 156 | int64_t victim_cpu); |
| 157 | |
| 158 | /* Open the gates for the workers */ |
| 159 | void test_fleet_start_all(struct test_fleet *f); |
| 160 | |
| 161 | struct test_verdict test_fleet_join(struct test_fleet *f); |
| 162 | |
| 163 | static inline bool test_fleet_must_stop(const struct test_fleet *f) { |
| 164 | return test_conc_must_stop(c: &f->conc); |
| 165 | } |
| 166 | |
| 167 | static inline bool test_fleet_must_park(const struct test_fleet *f) { |
| 168 | return test_conc_must_park(c: &f->conc); |
| 169 | } |
| 170 | |
| 171 | static inline void test_fleet_park(struct test_fleet *f, |
| 172 | struct test_conc_worker *w) { |
| 173 | test_conc_park(c: &f->conc, w); |
| 174 | } |
| 175 | |
| 176 | static inline void test_fleet_stop(struct test_fleet *f, |
| 177 | enum test_stop reason) { |
| 178 | test_conc_publish_stop(c: &f->conc, reason); |
| 179 | } |
| 180 | |
| 181 | /* First failure gets the message */ |
| 182 | void test_fleet_report(struct test_fleet *f, const char *file, uint32_t line, |
| 183 | const char *fmt, ...) cc_printf_like(4, 5); |
| 184 | |
| 185 | /* Runs a singular function on a given CPU from another thread's context, |
| 186 | * and then waits to join */ |
| 187 | bool test_run_on_core(cpu_id_t core_id, void (*fn)(void *), void *arg); |
| 188 | |
| 189 | /* NOTE: runner context only, NOT in the test */ |
| 190 | bool test_fleet_teardown(struct test_context *ctx); |
| 191 | |