The Pedigree Project 0.1
affinity-regressions.cc
1/* Copyright (c) 2026, Pedigree Developers. */
2#include <config.h>
3
4#if PEDIGREE_AFFINITY_TESTS && THREADS
5#include "pedigree/kernel/Atomic.h"
6#include "pedigree/kernel/LockGuard.h"
7#include "pedigree/kernel/Log.h"
8#include "pedigree/kernel/process/Process.h"
9#include "pedigree/kernel/process/Scheduler.h"
10#include "pedigree/kernel/process/Semaphore.h"
11#include "pedigree/kernel/process/Thread.h"
12#include "pedigree/kernel/process/WaitQueue.h"
13#include "pedigree/kernel/processor/Processor.h"
14#include "pedigree/kernel/processor/ProcessorInformation.h"
15#include "pedigree/kernel/time/Time.h"
16
17namespace {
18bool check(bool condition, const char* detail) {
19 if (!condition)
20 ERROR("AFFINITY-CORE: FAIL " << detail);
21 return condition;
22}
23
24bool sameMask(const CpuAffinityMask& left, const CpuAffinityMask& right) {
25 for (size_t i = 0; i < CpuAffinityMask::WordCount; ++i) {
26 if (left.words[i] != right.words[i])
27 return false;
28 }
29 return true;
30}
31
32struct WaitState {
33 WaitQueue queue;
34 Semaphore entered{0}, firstDone{0}, secondDone{0};
35 Atomic<size_t> returns{0};
36 Atomic<size_t> beforeGateCpu{CpuAffinityMask::MaximumCpus};
37 Atomic<size_t> firstCpu{CpuAffinityMask::MaximumCpus};
38 Atomic<size_t> secondCpu{CpuAffinityMask::MaximumCpus};
39 Atomic<size_t> firstReason{static_cast<size_t>(WaitQueue::WakeReason::Waiting)};
40 Atomic<size_t> secondReason{static_cast<size_t>(WaitQueue::WakeReason::Waiting)};
41 Atomic<bool> gatePassed{false};
42 bool repeat = false;
43};
44
45int waitEntry(void* parameter) {
46 auto& state = *static_cast<WaitState*>(parameter);
47 state.entered.release();
48 WaitQueue::WakeReason reason;
49 {
50 auto guard = state.queue.acquire();
51 reason = guard.wait(WaitQueue::Channel(&state, 1), Thread::EventWait);
52 }
53 state.firstReason = static_cast<size_t>(reason);
54 state.beforeGateCpu = Processor::index();
55 if (reason == WaitQueue::WakeReason::Signalled) {
56 // The ordinary wait and its guards have retired. This owned test entry
57 // has no CPU-local continuation or cleanup scope across the return gate.
58 const auto result = Processor::information().getCurrentThread()->completeAffinityAtSafePoint();
59 state.gatePassed = result == AffinityResult::Success && !Processor::getInterrupts();
60 state.firstCpu = Processor::index();
62 }
63 state.returns += 1;
64 state.firstDone.release();
65 if (!state.repeat || reason != WaitQueue::WakeReason::Signalled || !state.gatePassed)
66 return 0;
67 {
68 auto guard = state.queue.acquire();
69 reason = guard.wait(WaitQueue::Channel(&state, 2), Thread::EventWait);
70 }
71 state.secondReason = static_cast<size_t>(reason);
72 state.secondCpu = Processor::index();
73 state.returns += 1;
74 state.secondDone.release();
75 return 0;
76}
77
78Thread* startWaiter(WaitState& state, size_t cpu) {
79 ThreadPlacement placement;
80 placement.allowed.set(cpu);
81 placement.migratable = true;
82 Thread* current = Processor::information().getCurrentThread();
83 auto* peer =
84 new Thread(current->getParent(), waitEntry, &state, nullptr, false, false, true, &placement);
85 if (!peer)
86 return nullptr;
87 peer->setPriority(current->getPriority());
88 if (!peer->start()) {
89 peer->setUnwindState(Thread::TerminateThread);
90 if (!peer->joinForCompletion())
91 FATAL("AFFINITY-CORE: failed startup could not be joined");
92 return nullptr;
93 }
94 return peer;
95}
96
97bool waitForSleeping(Thread& peer, WaitState& state) {
98 const Time::Timestamp start = Time::getTicks();
99 while (Time::getTicks() - start < 5 * Time::Multiplier::Second) {
100 bool sleeping;
101 {
102 // A successful acquisition also waits for the outgoing-stack release.
103 LockGuard<Spinlock> guard(peer.getLock());
104 sleeping = peer.getStatus() == Thread::Sleeping;
105 }
106 if (sleeping && state.queue.waiterCount() == 1)
107 return true;
109 }
110 return false;
111}
112
113void retireWaiter(Thread* peer, WaitState& state, bool completed) {
114 if (!completed) {
116 state.queue.wakeAll();
117 }
118 if (!peer->joinForCompletion())
119 FATAL("AFFINITY-CORE: owned waiter could not be joined");
120}
121
122struct ReadyContext {
123 Thread* target = nullptr;
124 WaitState* state = nullptr;
125 CpuAffinityMask destination;
126 PerProcessorScheduler* source = nullptr;
127 bool moving = false;
128 size_t calls = 0;
129 bool passed = true;
130};
131
132ReadyContext* g_ReadyContext = nullptr;
133
134void beforeReadyPublication(Thread* peer) {
135 auto* context = __atomic_load_n(&g_ReadyContext, __ATOMIC_ACQUIRE);
136 if (!context || peer != context->target)
137 return;
138 ++context->calls;
139 if (context->calls != 1) {
140 context->passed = false;
141 return;
142 }
143 context->passed &=
144 check(Processor::getInterrupts() && !Processor::inDeviceHardIrq(), "ready hook context");
145 if (!context->passed)
146 return;
147
148 uint64_t generation = 0;
149 const AffinityResult requested = peer->requestAffinity(context->destination, generation);
150 context->passed &=
151 check(requested == AffinityResult::Success, "request while ready publication is held");
152 if (requested == AffinityResult::Success) {
153 context->passed &= check(peer->waitAffinity(generation) == AffinityResult::Success,
154 "affinity completion while wake is held");
155 }
156 ThreadPlacement placement;
157 peer->snapshotPlacement(placement);
158 bool correctOwner;
159 {
160 LockGuard<Spinlock> guard(peer->getLock());
161 correctOwner = peer->getScheduler() == context->source;
162 }
163 context->passed &= check(correctOwner && sameMask(placement.allowed, context->destination),
164 "mask acknowledged without moving the blocked kernel continuation");
165
166 // This is another real status publisher. It must not bypass the extracted
167 // notification, including when no second CPU exists to perform migration.
168 Scheduler::instance().threadStatusChanged(peer);
169 for (size_t i = 0; i < 8; ++i)
171 context->passed &= check(context->state->returns == 0 && context->state->queue.waiterCount() == 1,
172 "waiter executed or was reused before wake publication retired");
173}
174
175bool heldWake(size_t sourceCpu, size_t destinationCpu) {
176 WaitState state;
177 state.repeat = true;
178 Thread* peer = startWaiter(state, sourceCpu);
179 if (!check(peer != nullptr, "wake waiter allocation/start"))
180 return false;
181 bool passed = check(state.entered.acquireForCompletion(1, 5) && waitForSleeping(*peer, state),
182 "first wait enrollment");
183 ReadyContext context;
184 context.target = peer;
185 context.state = &state;
186 context.destination.set(destinationCpu);
187 context.moving = sourceCpu != destinationCpu;
188 {
189 LockGuard<Spinlock> guard(peer->getLock());
190 context.source = peer->getScheduler();
191 }
192 if (passed) {
193 __atomic_store_n(&g_ReadyContext, &context, __ATOMIC_RELEASE);
194 WaitQueue::setReadyPublicationHookForTest(peer, beforeReadyPublication);
195 const bool woke =
196 state.queue.wakeOne(WaitQueue::WakeReason::Signalled, WaitQueue::Channel(&state, 1));
197 WaitQueue::setReadyPublicationHookForTest(nullptr, nullptr);
198 __atomic_store_n(&g_ReadyContext, static_cast<ReadyContext*>(nullptr), __ATOMIC_RELEASE);
199 passed &= check(woke && context.calls == 1 && context.passed, "held wake admission");
200 }
201 bool completed = false;
202 if (passed) {
203 passed &=
204 check(state.firstDone.acquireForCompletion(1, 5) && state.returns == 1 &&
205 state.beforeGateCpu == sourceCpu && state.gatePassed &&
206 state.firstCpu == destinationCpu &&
207 state.firstReason == static_cast<size_t>(WaitQueue::WakeReason::Signalled) &&
208 waitForSleeping(*peer, state),
209 "source wait return, clean gate migration and second wait enrollment");
210 if (passed) {
211 const bool woke =
212 state.queue.wakeOne(WaitQueue::WakeReason::Signalled, WaitQueue::Channel(&state, 2));
213 completed = woke && state.secondDone.acquireForCompletion(1, 5);
214 passed &=
215 check(completed && state.returns == 2 && state.secondCpu == destinationCpu &&
216 state.secondReason == static_cast<size_t>(WaitQueue::WakeReason::Signalled) &&
217 state.queue.waiterCount() == 0,
218 "waiter reuse, single execution and final wake reason");
219 }
220 }
221 retireWaiter(peer, state, completed);
222 if (passed)
223 NOTICE("AFFINITY-CORE: PASS extracted wake " << (context.moving ? "migration" : "same CPU"));
224 return passed;
225}
226
227struct CancelContext {
228 Thread* target = nullptr;
229 Atomic<size_t> calls{0};
230 Atomic<bool> safeContext{true};
231};
232
233CancelContext* g_CancelContext = nullptr;
234
235void cancelBeforeCommit(Thread* peer) {
236 auto* context = __atomic_load_n(&g_CancelContext, __ATOMIC_ACQUIRE);
237 if (!context || peer != context->target)
238 return;
239 context->calls += 1;
240 context->safeContext = Processor::getInterrupts() && !Processor::inDeviceHardIrq();
242}
243
244bool cancelPending(size_t sourceCpu, size_t destinationCpu) {
245 WaitState state;
246 Thread* peer = startWaiter(state, sourceCpu);
247 if (!check(peer != nullptr, "cancel waiter allocation/start"))
248 return false;
249 bool passed = check(state.entered.acquireForCompletion(1, 5) && waitForSleeping(*peer, state),
250 "cancel wait enrollment");
251 CancelContext context;
252 context.target = peer;
253 CpuAffinityMask destination;
254 destination.set(destinationCpu);
255 if (passed) {
256 __atomic_store_n(&g_CancelContext, &context, __ATOMIC_RELEASE);
257 Thread::setAffinityCommitHookForTest(peer, cancelBeforeCommit);
258 uint64_t generation = 0;
259 const AffinityResult requested = peer->requestAffinity(destination, generation);
260 passed &= check(requested == AffinityResult::Success, "pending request admission");
261 if (requested == AffinityResult::Success) {
262 passed &= check(peer->waitAffinity(generation) == AffinityResult::Terminal,
263 "pending generation terminal completion");
264 }
265 Thread::setAffinityCommitHookForTest(nullptr, nullptr);
266 __atomic_store_n(&g_CancelContext, static_cast<CancelContext*>(nullptr), __ATOMIC_RELEASE);
267 passed &= check(context.calls == 1 && context.safeContext, "terminal precommit hook");
268 }
269 retireWaiter(peer, state, false);
270 if (passed)
271 NOTICE("AFFINITY-CORE: PASS pending terminal cancellation");
272 return passed;
273}
274
275int forbiddenEntry(void* parameter) {
276 *static_cast<bool*>(parameter) = true;
277 return 0;
278}
279
280bool pinnedAdmission() {
281 bool entered = false;
282 Thread* current = Processor::information().getCurrentThread();
283 auto* peer =
284 new Thread(current->getParent(), forbiddenEntry, &entered, nullptr, false, true, true);
285 if (!check(peer != nullptr, "pinned waiter allocation"))
286 return false;
287 ThreadPlacement before, after;
288 peer->snapshotPlacement(before);
289 uint64_t generation = 0;
290 const AffinityResult result = peer->requestAffinity(before.allowed, generation);
291 peer->snapshotPlacement(after);
292 bool passed = check(!before.migratable && result == AffinityResult::Pinned && !after.migratable &&
293 sameMask(before.allowed, after.allowed),
294 "fixed placement admission changed state");
296 if (!peer->joinForCompletion())
297 FATAL("AFFINITY-CORE: unstarted pinned waiter could not be joined");
298 passed &= check(!entered, "unstarted pinned waiter executed");
299 if (passed)
300 NOTICE("AFFINITY-CORE: PASS pinned admission");
301 return passed;
302}
303} // namespace
304
305EXPORTED_PUBLIC bool runAffinityRegressions() {
306 NOTICE("AFFINITY-CORE: BEGIN");
308 Processor::information().getCurrentThread(),
309 "fixture entry context"))
310 return false;
311 const size_t source = Processor::index();
312 const CpuAffinityMask online = Scheduler::onlineAffinity();
313 if (!check(online.contains(source), "source CPU online"))
314 return false;
315 size_t destination = source;
316 for (size_t cpu = 0; cpu < CpuAffinityMask::MaximumCpus; ++cpu) {
317 if (cpu != source && online.contains(cpu)) {
318 destination = cpu;
319 break;
320 }
321 }
322 bool passed = pinnedAdmission() && heldWake(source, source) && cancelPending(source, source);
323 if (destination != source) {
324 passed = passed && heldWake(source, destination) && cancelPending(source, destination);
325 } else {
326 NOTICE("AFFINITY-CORE: SKIP cross-CPU migration (one online CPU)");
327 }
328 if (passed)
329 NOTICE("AFFINITY-CORE: END PASS");
330 return passed;
331}
332#endif
static bool getInterrupts()
static ProcessorInformation & information()
static bool inDeviceHardIrq()
Definition Processor.h:581
static void setInterrupts(bool bEnable)
static size_t index()
static Scheduler & instance()
Definition Scheduler.h:96
void yield()
Definition Scheduler.cc:236
void setUnwindState(UnwindType ut)
Definition Thread.cc:3580
@ TerminateThread
Exit only this thread during Process exit.
Definition Thread.h:519
AffinityResult waitAffinity(uint64_t generation)
bool joinForCompletion()
Definition Thread.cc:2750
Status getStatus() const
Definition Thread.h:433
Process * getParent() const
Definition Thread.h:340
class PerProcessorScheduler * getScheduler() const
Definition Thread.h:929
AffinityResult requestAffinity(const CpuAffinityMask &mask, uint64_t &generation)
Spinlock & getLock()
Definition Thread.h:613