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
15struct test_fleet;
16struct 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 */
33struct 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
49struct 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 */
66typedef bool (*test_worker_fn)(struct test_fleet *f,
67 struct test_conc_worker *self);
68
69struct 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
112struct 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
125void *test_fleet_alloc(struct test_fleet *f, size_t size) cw_alloc(2);
126
127/* Spawn with custom worker options (affinity, priority class, flags) */
128struct test_conc_worker *
129test_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 */
146struct thread *test_fleet_worker_thread(struct test_fleet *f, const char *role);
147
148/* Spawn N many per CPU */
149size_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 */
154size_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 */
159void test_fleet_start_all(struct test_fleet *f);
160
161struct test_verdict test_fleet_join(struct test_fleet *f);
162
163static inline bool test_fleet_must_stop(const struct test_fleet *f) {
164 return test_conc_must_stop(c: &f->conc);
165}
166
167static inline bool test_fleet_must_park(const struct test_fleet *f) {
168 return test_conc_must_park(c: &f->conc);
169}
170
171static 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
176static 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 */
182void 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 */
187bool test_run_on_core(cpu_id_t core_id, void (*fn)(void *), void *arg);
188
189/* NOTE: runner context only, NOT in the test */
190bool test_fleet_teardown(struct test_context *ctx);
191