The Pedigree Project 0.1
log-regressions.cc
1/*
2 * Copyright (c) 2026, Pedigree Developers
3 *
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted.
6 */
7
8#include "pedigree/kernel/Atomic.h"
9#include "pedigree/kernel/Log.h"
10#include "pedigree/kernel/process/Scheduler.h"
11#include "pedigree/kernel/process/Semaphore.h"
12#include "pedigree/kernel/process/Thread.h"
13#include "pedigree/kernel/utilities/String.h"
14#include "pedigree/kernel/utilities/lib.h"
15
16namespace {
17constexpr size_t Attempts = 10000;
18constexpr char SnapshotA[] = "hosted-log-snapshot-a";
19constexpr char SnapshotB[] = "hosted-log-snapshot-b";
20
21bool check(bool condition, const char* detail) {
22 if (condition) {
23 return true;
24 }
25
26 ERROR("HOSTED-WAIT-TEST: FAIL log-callback-lifetime: " << detail);
27 return false;
28}
29
30struct RemovalContext;
31
32class BlockingLogger : public Log::LogCallback {
33 public:
34 explicit BlockingLogger(RemovalContext& context) : m_Context(context) {}
35
36 void callback(const LogCord&, bool) override;
37
38 private:
39 RemovalContext& m_Context;
40};
41
42struct RemovalWorker {
43 RemovalContext* context;
44};
45
46struct RemovalContext {
47 RemovalContext()
48 : logger(*this),
49 phase(0),
50 callbackCalls(0),
51 callbackAfterRemoval(0),
52 removerStarted(0),
53 removerReturned(0),
54 removersObservedWaiting(0),
55 failures(0),
56 removerA(nullptr),
57 removerB(nullptr) {
58 workerA.context = this;
59 workerB.context = this;
60 }
61
62 BlockingLogger logger;
63 Atomic<size_t> phase;
64 Atomic<size_t> callbackCalls;
65 Atomic<size_t> callbackAfterRemoval;
66 Atomic<size_t> removerStarted;
67 Atomic<size_t> removerReturned;
68 Atomic<size_t> removersObservedWaiting;
69 Atomic<size_t> failures;
70 Thread* removerA;
71 Thread* removerB;
72 RemovalWorker workerA;
73 RemovalWorker workerB;
74};
75
76RemovalContext* g_RemovalContext = nullptr;
77
78void BlockingLogger::callback(const LogCord&, bool) {
79 m_Context.callbackCalls += 1;
80 if (m_Context.removerReturned) {
81 m_Context.callbackAfterRemoval += 1;
82 }
83}
84
85bool removerWaiting(Thread* thread, Log::LogCallback* callback) {
86 Thread::WaitDebugInfo info = {};
87 uintptr_t debugAddress = 0;
88 return thread && thread->getWaitDebugInfo(info) && info.queue && info.channelOwner &&
89 info.queued && thread->getDebugState(debugAddress) == Thread::CallbackDrain &&
90 debugAddress == reinterpret_cast<uintptr_t>(callback);
91}
92
93void callbackPinHook(Log::LogCallback* callback) {
94 RemovalContext* context = g_RemovalContext;
95 if (!context || callback != &context->logger || !context->phase.compareAndSwap(0, 1)) {
96 return;
97 }
98
99 for (size_t attempt = 0; attempt < Attempts; ++attempt) {
100 if (context->removerStarted == static_cast<size_t>(2) &&
101 removerWaiting(context->removerA, callback) &&
102 removerWaiting(context->removerB, callback)) {
103 context->removersObservedWaiting += 1;
104 break;
105 }
107 }
108
109 if (!context->removersObservedWaiting) {
110 context->failures += 1;
111 }
112 context->phase = 2;
113}
114
115int removePinnedCallback(void* parameter) {
116 RemovalWorker* worker = reinterpret_cast<RemovalWorker*>(parameter);
117 RemovalContext* context = worker->context;
118 for (size_t attempt = 0; attempt < Attempts && context->phase == static_cast<size_t>(0);
119 ++attempt) {
121 }
122
123 if (context->phase == static_cast<size_t>(0)) {
124 context->failures += 1;
125 return 1;
126 }
127
128 context->removerStarted += 1;
129 if (!Log::instance().removeCallback(&context->logger)) {
130 context->failures += 1;
131 }
132 context->removerReturned += 1;
133 return 0;
134}
135
136class SelfRemovingLogger : public Log::LogCallback {
137 public:
138 SelfRemovingLogger() : calls(0), retryRequired(0) {}
139
140 void callback(const LogCord&, bool) override {
141 calls += 1;
142 if (!Log::instance().removeCallback(this)) {
143 retryRequired += 1;
144 }
145 }
146
147 Atomic<size_t> calls;
148 Atomic<size_t> retryRequired;
149};
150
151bool callbackLifetime() {
152 RemovalContext context;
153 if (!Log::instance().installCallback(&context.logger, true)) {
154 return check(false, "the test callback could not be registered");
155 }
156
157 Process* process = Scheduler::instance().getKernelProcess();
158 context.removerA =
159 new Thread(process, removePinnedCallback, &context.workerA, nullptr, false, true);
160 context.removerB =
161 new Thread(process, removePinnedCallback, &context.workerB, nullptr, false, true);
162 context.removerA->setName("hosted log callback remover A");
163 context.removerB->setName("hosted log callback remover B");
164
165 g_RemovalContext = &context;
166 Log::setCallbackPinHook(callbackPinHook);
167 NOTICE("hosted-log-callback-drain");
168
169 const bool joinedA = context.removerA->join();
170 const bool joinedB = context.removerB->join();
171 Log::setCallbackPinHook(nullptr);
172 g_RemovalContext = nullptr;
173
174 NOTICE("hosted-log-callback-after-removal");
175 Log::instance().removeCallback(&context.logger);
176
177 SelfRemovingLogger selfRemoving;
178 const bool selfInstalled = Log::instance().installCallback(&selfRemoving, true);
179 NOTICE("hosted-log-self-removal");
180 NOTICE("hosted-log-after-self-removal");
181 Log::instance().removeCallback(&selfRemoving);
182
183 bool passed = true;
184 passed &= check(joinedA && joinedB && context.failures == 0,
185 "concurrent callback removers did not complete cleanly");
186 passed &= check(context.removersObservedWaiting == 1 && context.removerReturned == 2,
187 "both external removers did not join the same callback drain");
188 passed &= check(context.callbackCalls == 1 && context.callbackAfterRemoval == 0,
189 "a callback ran after synchronous removal returned");
190 passed &= check(selfInstalled && selfRemoving.calls == 1 && selfRemoving.retryRequired == 1,
191 "self-removal did not close admission and require an external retry");
192
193 if (passed) {
194 NOTICE("HOSTED-WAIT-TEST: PASS log-callback-lifetime");
195 }
196 return passed;
197}
198
199struct SnapshotContext;
200
201class SnapshotLogger : public Log::LogCallback {
202 public:
203 explicit SnapshotLogger(SnapshotContext& context) : m_Context(context) {}
204
205 void callback(const LogCord& cord, bool) override;
206
207 private:
208 SnapshotContext& m_Context;
209};
210
211struct SnapshotContext {
212 SnapshotContext() : logger(*this), phase(0), sawA(0), sawB(0), unknown(0), worker(nullptr) {}
213
214 SnapshotLogger logger;
215 Atomic<size_t> phase;
216 Atomic<size_t> sawA;
217 Atomic<size_t> sawB;
218 Atomic<size_t> unknown;
219 Thread* worker;
220};
221
222SnapshotContext* g_SnapshotContext = nullptr;
223
224bool contains(const String& message, const char* needle, size_t length) {
225 if (message.length() < length) {
226 return false;
227 }
228
229 for (size_t i = 0; i <= (message.length() - length); ++i) {
230 if (StringCompareN(message.cstr() + i, needle, length) == 0) {
231 return true;
232 }
233 }
234 return false;
235}
236
237struct ReciprocalBacklogContext;
238
239class ReciprocalBacklogLogger : public Log::LogCallback {
240 public:
241 ReciprocalBacklogLogger(ReciprocalBacklogContext& context, bool first)
242 : m_Context(context), m_First(first) {}
243
244 void callback(const LogCord& cord, bool) override;
245
246 private:
247 ReciprocalBacklogContext& m_Context;
248 bool m_First;
249};
250
251struct ReciprocalBacklogWorker {
252 ReciprocalBacklogContext* context;
253 bool first;
254};
255
256struct ReciprocalBacklogContext {
257 ReciprocalBacklogContext()
258 : first(*this, true),
259 second(*this, false),
260 beginRemoval(0),
261 firstParticipated(0),
262 secondParticipated(0),
263 callbacksEntered(0),
264 backlogCallbacks(0),
265 removalRejections(0),
266 removalsFinished(0),
267 installsSucceeded(0),
268 installersFinished(0),
269 failures(0),
270 firstInstaller(nullptr),
271 secondInstaller(nullptr) {
272 firstWorker = {this, true};
273 secondWorker = {this, false};
274 }
275
276 ReciprocalBacklogLogger first;
277 ReciprocalBacklogLogger second;
278 Semaphore beginRemoval;
279 Atomic<size_t> firstParticipated;
280 Atomic<size_t> secondParticipated;
281 Atomic<size_t> callbacksEntered;
282 Atomic<size_t> backlogCallbacks;
283 Atomic<size_t> removalRejections;
284 Atomic<size_t> removalsFinished;
285 Atomic<size_t> installsSucceeded;
286 Atomic<size_t> installersFinished;
287 Atomic<size_t> failures;
288 Thread* firstInstaller;
289 Thread* secondInstaller;
290 ReciprocalBacklogWorker firstWorker;
291 ReciprocalBacklogWorker secondWorker;
292};
293
294void ReciprocalBacklogLogger::callback(const LogCord& cord, bool) {
295 Atomic<size_t>& participated =
296 m_First ? m_Context.firstParticipated : m_Context.secondParticipated;
297 if (!participated.compareAndSwap(0, 1)) {
298 return;
299 }
300
301 const String message = cord.toString();
302 if (contains(message, "(backlog) ", 10)) {
303 m_Context.backlogCallbacks += 1;
304 } else {
305 m_Context.failures += 1;
306 }
307
308 m_Context.callbacksEntered += 1;
309 if (!m_Context.beginRemoval.acquireForCompletion()) {
310 m_Context.failures += 1;
311 return;
312 }
313
314 Log::LogCallback* peer = m_First ? static_cast<Log::LogCallback*>(&m_Context.second)
315 : static_cast<Log::LogCallback*>(&m_Context.first);
316 if (!Log::instance().removeCallback(peer)) {
317 m_Context.removalRejections += 1;
318 } else {
319 m_Context.failures += 1;
320 }
321
322 m_Context.removalsFinished += 1;
323 for (size_t attempt = 0;
324 attempt < Attempts && m_Context.removalsFinished != static_cast<size_t>(2); ++attempt) {
326 }
327 if (m_Context.removalsFinished != static_cast<size_t>(2)) {
328 m_Context.failures += 1;
329 }
330}
331
332int installReciprocalBacklogCallback(void* parameter) {
333 ReciprocalBacklogWorker* worker = reinterpret_cast<ReciprocalBacklogWorker*>(parameter);
334 Log::LogCallback* callback = worker->first
335 ? static_cast<Log::LogCallback*>(&worker->context->first)
336 : static_cast<Log::LogCallback*>(&worker->context->second);
337 if (Log::instance().installCallback(callback, false)) {
338 worker->context->installsSucceeded += 1;
339 } else {
340 worker->context->failures += 1;
341 }
342 worker->context->installersFinished += 1;
343 return 0;
344}
345
346bool reciprocalBacklogRemoval() {
347 NOTICE("hosted-log-reciprocal-backlog-seed");
348
349 ReciprocalBacklogContext context;
350 Process* process = Scheduler::instance().getKernelProcess();
351 context.firstInstaller = new Thread(process, installReciprocalBacklogCallback,
352 &context.firstWorker, nullptr, false, true);
353 context.secondInstaller = new Thread(process, installReciprocalBacklogCallback,
354 &context.secondWorker, nullptr, false, true);
355 context.firstInstaller->setName("hosted log reciprocal backlog A");
356 context.secondInstaller->setName("hosted log reciprocal backlog B");
357
358 for (size_t attempt = 0; attempt < Attempts && context.callbacksEntered != static_cast<size_t>(2);
359 ++attempt) {
361 }
362 const bool bothEntered = context.callbacksEntered == static_cast<size_t>(2);
363 context.beginRemoval.release(2);
364
365 const bool firstJoined = context.firstInstaller->joinForCompletion();
366 const bool secondJoined = context.secondInstaller->joinForCompletion();
367 const bool firstOwnershipPreserved = !Log::instance().installCallback(&context.first, true);
368 const bool secondOwnershipPreserved = !Log::instance().installCallback(&context.second, true);
369 const bool firstRetired = Log::instance().removeCallback(&context.first);
370 const bool secondRetired = Log::instance().removeCallback(&context.second);
371 const bool firstReused = Log::instance().installCallback(&context.first, true);
372 const bool firstReuseRetired = Log::instance().removeCallback(&context.first);
373 const bool secondReused = Log::instance().installCallback(&context.second, true);
374 const bool secondReuseRetired = Log::instance().removeCallback(&context.second);
375
376 const bool passed =
377 check(bothEntered && firstJoined && secondJoined && firstOwnershipPreserved &&
378 secondOwnershipPreserved && firstRetired && secondRetired && firstReused &&
379 firstReuseRetired && secondReused && secondReuseRetired &&
380 context.backlogCallbacks == static_cast<size_t>(2) &&
381 context.removalRejections == static_cast<size_t>(2) &&
382 context.removalsFinished == static_cast<size_t>(2) &&
383 context.installsSucceeded == static_cast<size_t>(2) &&
384 context.installersFinished == static_cast<size_t>(2) && !context.failures,
385 "reciprocal backlog callbacks did not retain peer removal ownership");
386 if (passed) {
387 NOTICE("HOSTED-WAIT-TEST: PASS log-reciprocal-backlog-removal");
388 }
389 return passed;
390}
391
392void SnapshotLogger::callback(const LogCord& cord, bool) {
393 const String message = cord.toString();
394 if (contains(message, SnapshotA, sizeof(SnapshotA) - 1)) {
395 m_Context.sawA += 1;
396 } else if (contains(message, SnapshotB, sizeof(SnapshotB) - 1)) {
397 m_Context.sawB += 1;
398 } else {
399 m_Context.unknown += 1;
400 }
401}
402
403void entrySnapshotHook(const Log::LogEntry& entry) {
404 SnapshotContext* context = g_SnapshotContext;
405 if (!context || !(entry.str == SnapshotA) || !context->phase.compareAndSwap(0, 1)) {
406 return;
407 }
408
409 for (size_t attempt = 0; attempt < Attempts && context->phase != static_cast<size_t>(2);
410 ++attempt) {
412 }
413}
414
415int writeSecondSnapshot(void* parameter) {
416 SnapshotContext* context = reinterpret_cast<SnapshotContext*>(parameter);
417 for (size_t attempt = 0; attempt < Attempts && context->phase != static_cast<size_t>(1);
418 ++attempt) {
420 }
421 if (context->phase != static_cast<size_t>(1)) {
422 return 1;
423 }
424
425 NOTICE(SnapshotB);
426 context->phase = 2;
427 return 0;
428}
429
430bool entrySnapshotIsolation() {
431 SnapshotContext context;
432 context.worker = new Thread(Scheduler::instance().getKernelProcess(), writeSecondSnapshot,
433 &context, nullptr, false, true);
434 context.worker->setName("hosted concurrent log writer");
435 if (!Log::instance().installCallback(&context.logger, true)) {
436 context.phase = 1;
437 context.worker->join();
438 return check(false, "the snapshot callback could not be registered");
439 }
440
441 g_SnapshotContext = &context;
442 Log::setEntrySnapshotHook(entrySnapshotHook);
443 NOTICE(SnapshotA);
444 const bool joined = context.worker->join();
445 Log::setEntrySnapshotHook(nullptr);
446 g_SnapshotContext = nullptr;
447 Log::instance().removeCallback(&context.logger);
448
449 const bool passed = check(joined && context.phase == static_cast<size_t>(2) &&
450 context.sawA == 1 && context.sawB == 1 && context.unknown == 0,
451 "concurrent writers did not retain distinct entry snapshots");
452 if (passed) {
453 NOTICE("HOSTED-WAIT-TEST: PASS log-entry-snapshot");
454 }
455 return passed;
456}
457} // namespace
458
459bool runHostedLogRegressions() {
460 return callbackLifetime() && reciprocalBacklogRemoval() && entrySnapshotIsolation();
461}
the kernel's log
Definition Log.h:173
EXPORTED_PUBLIC bool removeCallback(LogCallback *pCallback)
Definition Log.cc:228
static EXPORTED_PUBLIC Log & instance()
Definition Log.cc:117
EXPORTED_PUBLIC bool installCallback(LogCallback *pCallback, bool bSkipBacklog=false)
Definition Log.cc:145
static Scheduler & instance()
Definition Scheduler.h:96
void yield()
Definition Scheduler.cc:226
bool getWaitDebugInfo(WaitDebugInfo &info)
Definition Thread.cc:3184
DebugState getDebugState(uintptr_t &address)
Definition Thread.h:570