The Pedigree Project 0.1
cache-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/TargetInfo.h"
11#include "pedigree/kernel/process/Scheduler.h"
12#include "pedigree/kernel/process/Semaphore.h"
13#include "pedigree/kernel/process/Thread.h"
14#include "pedigree/kernel/processor/PhysicalMemoryManager.h"
15#include "pedigree/kernel/processor/VirtualAddressSpace.h"
16#include "pedigree/kernel/time/Time.h"
17#include "pedigree/kernel/utilities/Cache.h"
18
20 public:
21 static void start(CacheManager& manager) {
22 auto guard = manager.m_TrimWaiters.acquire();
23 manager.m_bActive = true;
24 }
25
26 static void stop(CacheManager& manager) {
27 manager.stopPeriodicWork();
28 }
29
30 static void tick(CacheManager& manager, uint64_t delta, bool pressure) {
31 manager.timerTick(delta, pressure);
32 }
33
34 static bool state(CacheManager& manager, bool requested, uint64_t delta) {
35 auto guard = manager.m_TrimWaiters.acquire();
36 return manager.m_bTrimRequested == requested && manager.m_TrimDelta == delta;
37 }
38};
39
40namespace {
41constexpr size_t PageSize = TargetInfo::getPageSize();
42
43bool checkNamed(bool condition, const char* test, const char* detail) {
44 if (condition) {
45 return true;
46 }
47
48 ERROR("HOSTED-WAIT-TEST: FAIL " << test << ": " << detail);
49 return false;
50}
51
52bool check(bool condition, const char* detail) {
53 return checkNamed(condition, "cache-callback-lifetime", detail);
54}
55
56bool waitUntilQueued(Thread* thread, size_t debugState) {
57 const Time::Timestamp deadline = Time::getTicks() + (500 * Time::Multiplier::Millisecond);
58 while (Time::getTicks() < deadline) {
59 Thread::WaitDebugInfo info = {};
60 uintptr_t debugAddress = 0;
61 if (thread->getWaitDebugInfo(info) && info.queue && info.queued &&
62 thread->getDebugState(debugAddress) == debugState) {
63 return true;
64 }
66 }
67 return false;
68}
69
70bool waitUntilQueuedAt(Thread* thread, size_t debugState, uintptr_t debugAddress) {
71 const Time::Timestamp deadline = Time::getTicks() + (500 * Time::Multiplier::Millisecond);
72 while (Time::getTicks() < deadline) {
73 Thread::WaitDebugInfo info = {};
74 uintptr_t address = 0;
75 if (thread->getWaitDebugInfo(info) && info.queue && info.queued &&
76 thread->getDebugState(address) == debugState && address == debugAddress) {
77 return true;
78 }
80 }
81 return false;
82}
83
84struct CacheTrimWakeContext {
85 CacheManager manager;
86 size_t blocks = 0;
87 bool passed = true;
88};
89
90CacheTrimWakeContext* g_CacheTrimWakeContext = nullptr;
91
92void cacheTrimBeforeBlock(WaitQueue*, Thread* thread, const WaitQueue::Channel& channel, size_t) {
93 CacheTrimWakeContext* context = g_CacheTrimWakeContext;
94 if (!context || channel.owner != &context->manager)
95 return;
96
97 constexpr const char* Test = "cache-manager-trim-wake-gating";
98 constexpr uint64_t Tick = 1000000ULL;
99 constexpr uint64_t Period = CACHE_WRITEBACK_PERIOD * Tick;
100 CacheManager& manager = context->manager;
101 const size_t block = ++context->blocks;
102 const uint64_t expectedDelta = block == 2 ? Period - Tick : 0;
103 context->passed &= checkNamed(CacheManagerTestPeer::state(manager, false, expectedDelta), Test,
104 "worker lost elapsed time or did not consume pending work");
105
106 Thread::WaitDebugInfo info = {};
107 if (block == 1) {
108 for (size_t tick = 1; tick < CACHE_WRITEBACK_PERIOD; ++tick)
109 CacheManagerTestPeer::tick(manager, Tick, false);
110 context->passed &= checkNamed(CacheManagerTestPeer::state(manager, false, Period - Tick) &&
111 thread->getWaitDebugInfo(info) && info.queued &&
112 info.reason == WaitQueue::WakeReason::Waiting,
113 Test, "healthy subperiod ticks woke the worker");
114
115 CacheManagerTestPeer::tick(manager, 0, true);
116 CacheManagerTestPeer::tick(manager, 0, false);
117 context->passed &= checkNamed(
118 CacheManagerTestPeer::state(manager, true, Period - Tick) &&
119 thread->getWaitDebugInfo(info) && info.reason == WaitQueue::WakeReason::Signalled,
120 Test, "pressure did not wake the worker or a later tick erased the request");
121 } else if (block == 2) {
122 CacheManagerTestPeer::tick(manager, Tick, false);
123 context->passed &= checkNamed(
124 CacheManagerTestPeer::state(manager, true, Period) && thread->getWaitDebugInfo(info) &&
125 info.reason == WaitQueue::WakeReason::Signalled,
126 Test, "writeback did not wake at the accumulated period boundary");
127 } else {
128 context->passed &= checkNamed(block == 3, Test, "worker woke more often than requested");
129 CacheManagerTestPeer::stop(manager);
130 context->passed &= checkNamed(
131 thread->getWaitDebugInfo(info) && info.reason == WaitQueue::WakeReason::Signalled, Test,
132 "stopping periodic work did not wake the idle worker");
133 }
134
135 // Always release the published wait, including when the assertion fails.
136 if (!context->passed)
137 CacheManagerTestPeer::stop(manager);
138}
139
140bool cacheManagerTrimWakeGating() {
141 constexpr const char* Test = "cache-manager-trim-wake-gating";
142 constexpr uint64_t Period = CACHE_WRITEBACK_PERIOD * 1000000ULL;
143 CacheTrimWakeContext context;
144 CacheManagerTestPeer::tick(context.manager, Period, false);
145 CacheManagerTestPeer::tick(context.manager, 0, false);
146 context.passed = checkNamed(CacheManagerTestPeer::state(context.manager, true, Period), Test,
147 "due work was lost while no worker was waiting");
148 CacheManagerTestPeer::start(context.manager);
149 g_CacheTrimWakeContext = &context;
150 WaitQueue::setBeforeBlockHook(cacheTrimBeforeBlock);
151 context.manager.trimThread();
152 WaitQueue::setBeforeBlockHook(nullptr);
153 g_CacheTrimWakeContext = nullptr;
154
155 context.passed &= checkNamed(context.blocks == 3, Test,
156 "worker did not complete pressure, writeback and stop transitions");
157 if (context.passed)
158 NOTICE("HOSTED-WAIT-TEST: PASS " << Test);
159 return context.passed;
160}
161
162struct CacheLifetimeContext {
163 CacheLifetimeContext()
164 : cache(nullptr),
165 callbackEntered(0),
166 allowCallbackReturn(0),
167 callbackCalls(0),
168 evictionCalls(0),
169 reentrantPins(0),
170 deleteReturned(0) {}
171
172 Cache* cache;
173 Semaphore callbackEntered;
174 Semaphore allowCallbackReturn;
175 Atomic<size_t> callbackCalls;
176 Atomic<size_t> evictionCalls;
177 Atomic<size_t> reentrantPins;
178 Atomic<size_t> deleteReturned;
179};
180
181bool cacheCallback(CacheConstants::CallbackCause cause, uintptr_t loc, uintptr_t, void* parameter) {
182 CacheLifetimeContext* context = reinterpret_cast<CacheLifetimeContext*>(parameter);
183 if (cause == CacheConstants::Eviction) {
184 context->evictionCalls += 1;
185 } else if (cause == CacheConstants::WriteBack) {
186 uintptr_t page = context->cache->lookup(loc);
187 if (page) {
188 context->reentrantPins += 1;
189 context->cache->release(loc);
190 }
191 }
192 const size_t call = (context->callbackCalls += 1);
193 if (call == 1) {
194 context->callbackEntered.release();
195 const bool released = context->allowCallbackReturn.acquireForCompletion();
196 (void)released;
197 }
198 return true;
199}
200
201int deleteCache(void* parameter) {
202 CacheLifetimeContext* context = reinterpret_cast<CacheLifetimeContext*>(parameter);
203 delete context->cache;
204 context->deleteReturned += 1;
205 return 0;
206}
207
208bool callbackLifetime() {
209 CacheLifetimeContext context;
210 context.cache = new Cache;
211 context.cache->setCallback(cacheCallback, &context);
212
213 constexpr uintptr_t Key = 0xCA7E000;
214 const uintptr_t page = context.cache->insert(Key);
215 if (!check(page != 0, "could not create the test cache page")) {
216 delete context.cache;
217 return false;
218 }
219
220 context.cache->markNoLongerEditing(Key);
221 context.cache->triggerChecksum(Key);
222 reinterpret_cast<uint8_t*>(page)[0] ^= 0xA5;
223 context.cache->sync(Key, true);
224
225 if (!check(context.callbackEntered.acquire(1, 2),
226 "the queued writeback callback did not start")) {
227 // Keep cleanup safe if the callback crossed the timeout boundary.
228 context.allowCallbackReturn.release();
229 delete context.cache;
230 return false;
231 }
232
233 Thread* deleter = new Thread(Scheduler::instance().getKernelProcess(), deleteCache, &context,
234 nullptr, false, true);
235 deleter->setName("hosted Cache callback-drain deleter");
236
237 const bool drainPublished = waitUntilQueued(deleter, Thread::CallbackDrain);
238 const bool callbackPinnedObject = context.deleteReturned == 0;
239
240 context.allowCallbackReturn.release();
241 const bool joined = deleter->join();
242
243 const bool passed =
244 check(drainPublished, "destruction did not publish its callback-drain wait") &&
245 check(callbackPinnedObject, "Cache destruction returned while its callback was active") &&
246 check(joined, "the Cache deleter did not become reapable") &&
247 check(context.deleteReturned == 1, "Cache destruction did not complete exactly once") &&
248 check(context.callbackCalls == 2,
249 "writeback and eviction callbacks did not execute exactly once") &&
250 check(context.reentrantPins == 1, "dirty writeback could not safely re-enter the Cache") &&
251 check(context.evictionCalls == 1, "Cache destruction did not reclaim the inserted page");
252
253 if (passed) {
254 NOTICE("HOSTED-WAIT-TEST: PASS cache-callback-lifetime");
255 }
256 return passed;
257}
258
259struct QueuedLifetimeContext {
260 QueuedLifetimeContext()
261 : target(nullptr),
262 blockerEntered(0),
263 allowBlockerReturn(0),
264 blockerCalls(0),
265 targetCalls(0),
266 deleteReturned(0) {}
267
268 Cache* target;
269 Semaphore blockerEntered;
270 Semaphore allowBlockerReturn;
271 Atomic<size_t> blockerCalls;
272 Atomic<size_t> targetCalls;
273 Atomic<size_t> deleteReturned;
274};
275
276bool blockerCallback(CacheConstants::CallbackCause, uintptr_t, uintptr_t, void* parameter) {
277 QueuedLifetimeContext* context = reinterpret_cast<QueuedLifetimeContext*>(parameter);
278 if ((context->blockerCalls += 1) == 1) {
279 context->blockerEntered.release();
280 const bool released = context->allowBlockerReturn.acquireForCompletion();
281 (void)released;
282 }
283 return true;
284}
285
286bool queuedTargetCallback(CacheConstants::CallbackCause, uintptr_t, uintptr_t, void* parameter) {
287 QueuedLifetimeContext* context = reinterpret_cast<QueuedLifetimeContext*>(parameter);
288 context->targetCalls += 1;
289 return true;
290}
291
292int deleteQueuedCache(void* parameter) {
293 QueuedLifetimeContext* context = reinterpret_cast<QueuedLifetimeContext*>(parameter);
294 delete context->target;
295 context->deleteReturned += 1;
296 return 0;
297}
298
299bool queuedRequestLifetime() {
300 QueuedLifetimeContext context;
301 Cache blocker;
302 blocker.setCallback(blockerCallback, &context);
303
304 constexpr uintptr_t BlockerKey = 0xCA7E010;
305 constexpr uintptr_t TargetKey = 0xCA7E020;
306 if (!checkNamed(blocker.insert(BlockerKey) != 0, "cache-queued-lifetime",
307 "could not create the worker-blocking page")) {
308 return false;
309 }
310 blocker.markNoLongerEditing(BlockerKey);
311 blocker.markDirty(BlockerKey);
312 blocker.sync(BlockerKey, true);
313 if (!checkNamed(context.blockerEntered.acquire(1, 2), "cache-queued-lifetime",
314 "the blocking Cache callback did not start")) {
315 context.allowBlockerReturn.release();
316 return false;
317 }
318
319 context.target = new Cache;
320 context.target->setCallback(queuedTargetCallback, &context);
321 if (!checkNamed(context.target->insert(TargetKey) != 0, "cache-queued-lifetime",
322 "could not create the queued target page")) {
323 context.allowBlockerReturn.release();
324 delete context.target;
325 return false;
326 }
327 context.target->markNoLongerEditing(TargetKey);
328 context.target->markDirty(TargetKey);
329 context.target->sync(TargetKey, true);
330
331 Thread* deleter = new Thread(Scheduler::instance().getKernelProcess(), deleteQueuedCache,
332 &context, nullptr, false, true);
333 deleter->setName("hosted queued Cache lease deleter");
334
335 const bool queuedLeasePublished = waitUntilQueued(deleter, Thread::CallbackDrain);
336 const bool queuedRequestPinnedObject = context.deleteReturned == 0;
337
338 context.allowBlockerReturn.release();
339 const bool joined = deleter->join();
340
341 const bool passed = checkNamed(queuedLeasePublished, "cache-queued-lifetime",
342 "destruction did not wait for a queued request lease") &&
343 checkNamed(queuedRequestPinnedObject, "cache-queued-lifetime",
344 "a queued request did not pin its Cache") &&
345 checkNamed(joined && context.deleteReturned == 1, "cache-queued-lifetime",
346 "queued Cache destruction did not complete") &&
347 checkNamed(context.targetCalls == 2, "cache-queued-lifetime",
348 "the queued writeback and final eviction did not both execute");
349
350 if (passed) {
351 NOTICE("HOSTED-WAIT-TEST: PASS cache-queued-lifetime");
352 }
353 return passed;
354}
355
356bool emptyAndReuse() {
357 Cache cache;
358 constexpr uintptr_t FirstKey = 0xCA7E100;
359 constexpr uintptr_t SecondKey = 0xCA7E200;
360
361 const uintptr_t firstPage = cache.insert(FirstKey);
362 if (!checkNamed(firstPage != 0, "cache-empty-reuse",
363 "could not create the first no-callback page")) {
364 return false;
365 }
366
367 cache.markNoLongerEditing(FirstKey);
368 cache.triggerChecksum(FirstKey);
369 reinterpret_cast<uint8_t*>(firstPage)[0] ^= 0x5A;
370 cache.empty();
371
372 const uintptr_t secondPage = cache.insert(SecondKey);
373 const bool reused = checkNamed(secondPage != 0, "cache-empty-reuse",
374 "could not insert after emptying a dirty Cache") &&
375 checkNamed(cache.exists(SecondKey, PageSize), "cache-empty-reuse",
376 "the replacement page was not published");
377 cache.empty();
378
379 if (reused) {
380 NOTICE("HOSTED-WAIT-TEST: PASS cache-empty-reuse");
381 }
382 return reused;
383}
384
385struct RetirementContext {
386 RetirementContext()
387 : cache(nullptr),
388 evictionEntered(0),
389 allowEvictionReturn(0),
390 evictionCalls(0),
391 evictReturned(0),
392 insertReturned(0),
393 replacementPage(0) {}
394
395 Cache* cache;
396 Semaphore evictionEntered;
397 Semaphore allowEvictionReturn;
398 Atomic<size_t> evictionCalls;
399 Atomic<size_t> evictReturned;
400 Atomic<size_t> insertReturned;
401 uintptr_t replacementPage;
402};
403
404bool retirementCallback(CacheConstants::CallbackCause cause, uintptr_t, uintptr_t,
405 void* parameter) {
406 RetirementContext* context = reinterpret_cast<RetirementContext*>(parameter);
407 if (cause == CacheConstants::Eviction && (context->evictionCalls += 1) == 1) {
408 context->evictionEntered.release();
409 const bool released = context->allowEvictionReturn.acquireForCompletion();
410 (void)released;
411 }
412 return true;
413}
414
415int evictRetirementPage(void* parameter) {
416 RetirementContext* context = reinterpret_cast<RetirementContext*>(parameter);
417 context->evictReturned += context->cache->evict(0xCA7E300) ? 1 : 2;
418 return 0;
419}
420
421int insertRetirementReplacement(void* parameter) {
422 RetirementContext* context = reinterpret_cast<RetirementContext*>(parameter);
423 context->replacementPage = context->cache->insert(0xCA7E300);
424 context->insertReturned += 1;
425 return 0;
426}
427
428bool retirementPublication() {
429 RetirementContext context;
430 Cache cache;
431 context.cache = &cache;
432 cache.setCallback(retirementCallback, &context);
433
434 constexpr uintptr_t Key = 0xCA7E300;
435 const uintptr_t originalPage = cache.insert(Key);
436 if (!checkNamed(originalPage != 0, "cache-retirement-publication",
437 "could not create the original page")) {
438 return false;
439 }
440 cache.markNoLongerEditing(Key);
441
442 Thread* evictor = new Thread(Scheduler::instance().getKernelProcess(), evictRetirementPage,
443 &context, nullptr, false, true);
444 evictor->setName("hosted Cache retirement evictor");
445
446 const bool callbackEntered = context.evictionEntered.acquire(1, 2);
447 if (!callbackEntered) {
448 context.allowEvictionReturn.release();
449 evictor->join();
450 cache.empty();
451 return checkNamed(false, "cache-retirement-publication",
452 "the eviction callback did not publish retirement");
453 }
454
455 const bool retiringPinRejected = !cache.pin(Key);
456 const bool retiringLookupRejected = cache.lookup(Key) == 0;
457
458 Thread* inserter = new Thread(Scheduler::instance().getKernelProcess(),
459 insertRetirementReplacement, &context, nullptr, false, true);
460 inserter->setName("hosted Cache same-key replacement");
461
462 const bool insertWaitPublished = waitUntilQueued(inserter, Thread::CallbackDrain);
463 const bool replacementBlocked = context.insertReturned == 0;
464
465 context.allowEvictionReturn.release();
466 const bool evictorJoined = evictor->join();
467 const bool inserterJoined = inserter->join();
468
469 const bool passed =
470 checkNamed(retiringPinRejected && retiringLookupRejected, "cache-retirement-publication",
471 "a retiring page remained available to a new consumer") &&
472 checkNamed(insertWaitPublished, "cache-retirement-publication",
473 "same-key insertion did not wait for retirement publication") &&
474 checkNamed(replacementBlocked, "cache-retirement-publication",
475 "same-key insertion returned the retiring page") &&
476 checkNamed(evictorJoined && context.evictReturned == 1, "cache-retirement-publication",
477 "the original eviction did not complete successfully") &&
478 checkNamed(inserterJoined && context.insertReturned == 1 && context.replacementPage != 0,
479 "cache-retirement-publication", "the replacement insertion did not complete") &&
480 checkNamed(cache.exists(Key, PageSize), "cache-retirement-publication",
481 "the replacement page was invalidated by the old callback");
482
483 cache.empty();
484 if (passed) {
485 NOTICE("HOSTED-WAIT-TEST: PASS cache-retirement-publication");
486 }
487 return passed;
488}
489
490struct DiscardEditingContext {
491 DiscardEditingContext() : writebacks(0), evictions(0) {}
492
493 Atomic<size_t> writebacks;
494 Atomic<size_t> evictions;
495};
496
497bool discardEditingCallback(CacheConstants::CallbackCause cause, uintptr_t, uintptr_t,
498 void* parameter) {
499 DiscardEditingContext* context = reinterpret_cast<DiscardEditingContext*>(parameter);
500 if (cause == CacheConstants::WriteBack) {
501 context->writebacks += 1;
502 } else if (cause == CacheConstants::Eviction) {
503 context->evictions += 1;
504 }
505 return true;
506}
507
508bool failedPublicationDiscard() {
509 constexpr uintptr_t Key = 0xCA7E400;
510 Cache cache;
511 DiscardEditingContext context;
512 cache.setCallback(discardEditingCallback, &context);
513
514 const uintptr_t failedPage = cache.insert(Key);
515 const bool ordinaryRejected = failedPage != 0 && !cache.evict(Key) && cache.exists(Key, PageSize);
516 const bool discarded = ordinaryRejected && cache.discardEditing(Key);
517 const bool removed = !cache.exists(Key, PageSize);
518 const bool suppressedWriteback = context.writebacks == 0 && context.evictions == 1;
519
520 const uintptr_t pinnedPage = cache.insert(Key);
521 const bool pinned = pinnedPage != 0 && cache.pin(Key);
522 const bool rejectedPinned = pinned && !cache.discardEditing(Key);
523 if (pinned) {
524 cache.release(Key);
525 }
526 const bool discardedAfterRelease = rejectedPinned && cache.discardEditing(Key);
527
528 const uintptr_t publishedPage = cache.insert(Key);
529 if (publishedPage) {
530 cache.markNoLongerEditing(Key);
531 }
532 const bool rejectedPublished = publishedPage != 0 && !cache.discardEditing(Key);
533 cache.empty();
534
535 const bool passed =
536 checkNamed(ordinaryRejected, "cache-failed-publication-discard",
537 "ordinary eviction reclaimed an unpublished Editing page") &&
538 checkNamed(discarded && removed, "cache-failed-publication-discard",
539 "an unpinned Editing page was not synchronously removed") &&
540 checkNamed(suppressedWriteback, "cache-failed-publication-discard",
541 "discarding failed data invoked backing-store writeback") &&
542 checkNamed(discardedAfterRelease, "cache-failed-publication-discard",
543 "discard removed a pinned page or could not remove it after release") &&
544 checkNamed(rejectedPublished, "cache-failed-publication-discard",
545 "discard removed a page after successful publication");
546
547 if (passed) {
548 NOTICE("HOSTED-WAIT-TEST: PASS cache-failed-publication-discard");
549 }
550 return passed;
551}
552
553struct RetirePublicationContext {
554 RetirePublicationContext()
555 : cache(nullptr),
556 key(0),
557 page(0),
558 admissionEntered(0),
559 allowPublication(0),
560 callbackEntered(0),
561 allowCallbackReturn(0),
562 admissionCalls(0),
563 queuedCallbacks(0),
564 queuedCallbackFinished(0),
565 evictionCalls(0),
566 retireCallbacks(0),
567 retireSawQueuedCompletion(0),
568 retireArgumentsValid(0),
569 syncReturned(0),
570 retireReturned(0),
571 retireSucceeded(0) {}
572
573 Cache* cache;
574 uintptr_t key;
575 uintptr_t page;
576 Semaphore admissionEntered;
577 Semaphore allowPublication;
578 Semaphore callbackEntered;
579 Semaphore allowCallbackReturn;
580 Atomic<size_t> admissionCalls;
581 Atomic<size_t> queuedCallbacks;
582 Atomic<size_t> queuedCallbackFinished;
583 Atomic<size_t> evictionCalls;
584 Atomic<size_t> retireCallbacks;
585 Atomic<size_t> retireSawQueuedCompletion;
586 Atomic<size_t> retireArgumentsValid;
587 Atomic<size_t> syncReturned;
588 Atomic<size_t> retireReturned;
589 Atomic<size_t> retireSucceeded;
590};
591
592void retireAdmissionHook(Cache* cache, uintptr_t key, void* parameter) {
593 RetirePublicationContext* context = reinterpret_cast<RetirePublicationContext*>(parameter);
594 if (cache == context->cache && key == context->key && (context->admissionCalls += 1) == 1) {
595 context->admissionEntered.release();
596 const bool released = context->allowPublication.acquireForCompletion();
597 (void)released;
598 }
599}
600
601bool retireQueuedCallback(CacheConstants::CallbackCause cause, uintptr_t, uintptr_t,
602 void* parameter) {
603 RetirePublicationContext* context = reinterpret_cast<RetirePublicationContext*>(parameter);
604 if (cause == CacheConstants::Eviction) {
605 context->evictionCalls += 1;
606 } else if (cause == CacheConstants::WriteBack && (context->queuedCallbacks += 1) == 1) {
607 context->callbackEntered.release();
608 const bool released = context->allowCallbackReturn.acquireForCompletion();
609 (void)released;
610 context->queuedCallbackFinished = 1;
611 }
612 return true;
613}
614
615bool retireSynchronousCallback(uintptr_t key, uintptr_t page, void* parameter) {
616 RetirePublicationContext* context = reinterpret_cast<RetirePublicationContext*>(parameter);
617 context->retireCallbacks += 1;
618 context->retireSawQueuedCompletion = context->queuedCallbackFinished;
619 context->retireArgumentsValid = key == context->key && page == context->page;
620 return true;
621}
622
623int publishRetireWriteback(void* parameter) {
624 RetirePublicationContext* context = reinterpret_cast<RetirePublicationContext*>(parameter);
625 context->cache->sync(context->key, true);
626 context->syncReturned += 1;
627 return 0;
628}
629
630int retirePublishedWriteback(void* parameter) {
631 RetirePublicationContext* context = reinterpret_cast<RetirePublicationContext*>(parameter);
632 if (context->cache->retireWriteback(context->key, retireSynchronousCallback, context)) {
633 context->retireSucceeded += 1;
634 }
635 context->retireReturned += 1;
636 return 0;
637}
638
639bool retirePrepublicationWriteback() {
640 constexpr uintptr_t Key = 0xCA7E500;
641 RetirePublicationContext context;
642 Cache cache;
643 context.cache = &cache;
644 context.key = Key;
645 cache.setCallback(retireQueuedCallback, &context);
646
647 context.page = cache.insert(Key);
648 if (!checkNamed(context.page != 0, "cache-retire-prepublication",
649 "could not create the test page")) {
650 return false;
651 }
652 cache.markNoLongerEditing(Key);
653 cache.markDirty(Key);
654 cache.startAtomic();
655 cache.setWritebackAdmissionHookForTest(retireAdmissionHook, &context);
656
657 Thread* producer = new Thread(Scheduler::instance().getKernelProcess(), publishRetireWriteback,
658 &context, nullptr, false, true);
659 producer->setName("hosted Cache paused writeback producer");
660 const bool admissionPaused = context.admissionEntered.acquire(1, 2);
661 if (!admissionPaused) {
662 context.allowPublication.release();
663 context.allowCallbackReturn.release();
664 producer->join();
665 cache.setWritebackAdmissionHookForTest(nullptr, nullptr);
666 cache.endAtomic();
667 cache.empty();
668 return checkNamed(false, "cache-retire-prepublication",
669 "sync did not pause after publishing its page reference");
670 }
671
672 Thread* retirer = new Thread(Scheduler::instance().getKernelProcess(), retirePublishedWriteback,
673 &context, nullptr, false, true);
674 retirer->setName("hosted Cache writeback retirer");
675 const bool drainPublished = waitUntilQueuedAt(retirer, Thread::CallbackDrain, Key);
676 const bool blockedBeforePublication = context.retireReturned == 0;
677 if (drainPublished) {
678 cache.sync(Key, true);
679 }
680 const bool drainingSyncRejected = context.admissionCalls == 1;
681
682 context.allowPublication.release();
683 const bool queuedCallbackEntered = context.callbackEntered.acquire(1, 2);
684 const bool blockedThroughCallback = context.retireReturned == 0;
685 context.allowCallbackReturn.release();
686
687 const bool producerJoined = producer->join();
688 const bool retirerJoined = retirer->join();
689 cache.setWritebackAdmissionHookForTest(nullptr, nullptr);
690 cache.endAtomic();
691
692 const bool passed =
693 checkNamed(drainPublished, "cache-retire-prepublication",
694 "retirement did not publish its exact per-key CallbackDrain wait") &&
695 checkNamed(blockedBeforePublication, "cache-retire-prepublication",
696 "retirement returned before a visible writeback was queued") &&
697 checkNamed(drainingSyncRejected, "cache-retire-prepublication",
698 "sync admitted another writeback while retirement was draining") &&
699 checkNamed(queuedCallbackEntered && blockedThroughCallback, "cache-retire-prepublication",
700 "retirement did not wait for the queued callback to finish") &&
701 checkNamed(producerJoined && retirerJoined, "cache-retire-prepublication",
702 "writeback producer or retirer did not become reapable") &&
703 checkNamed(
704 context.syncReturned == 1 && context.retireReturned == 1 && context.retireSucceeded == 1,
705 "cache-retire-prepublication", "retirement did not complete exactly once") &&
706 checkNamed(context.queuedCallbacks == 1 && context.queuedCallbackFinished == 1 &&
707 context.retireCallbacks == 1 && context.retireSawQueuedCompletion == 1,
708 "cache-retire-prepublication",
709 "the synchronous retirement callback overtook queued writeback") &&
710 checkNamed(context.retireArgumentsValid == 1 && context.evictionCalls == 1,
711 "cache-retire-prepublication",
712 "retirement callback arguments or final eviction were incorrect") &&
713 checkNamed(!cache.exists(Key, PageSize) && cache.lookup(Key) == 0,
714 "cache-retire-prepublication", "the successful page remained published");
715
716 if (cache.exists(Key, PageSize)) {
717 cache.empty();
718 }
719 if (passed) {
720 NOTICE("HOSTED-WAIT-TEST: PASS cache-retire-prepublication");
721 }
722 return passed;
723}
724
725struct DiscardPublicationContext {
726 explicit DiscardPublicationContext(bool cancelPlan)
727 : publication(), prepared(0), allowCompletion(0), completed(0), cancel(cancelPlan) {}
728
729 RetirePublicationContext publication;
730 Semaphore prepared;
731 Semaphore allowCompletion;
732 Atomic<size_t> completed;
733 bool cancel;
734};
735
736int preparePublishedDiscard(void* parameter) {
737 auto* context = reinterpret_cast<DiscardPublicationContext*>(parameter);
738 auto& publication = context->publication;
739 const Cache::DiscardReference reference = {publication.key, 1};
741 const auto status = publication.cache->prepareDiscardFrom(publication.key, &reference, 1, plan);
742 if (status == Cache::DiscardStatus::Ready && plan) {
743 publication.retireSucceeded += 1;
744 }
745 publication.retireReturned += 1;
746 context->prepared.release();
747 const bool released = context->allowCompletion.acquireForCompletion();
748 (void)released;
749 if (plan && !context->cancel) {
750 plan.get()->commit();
751 }
752 // The plan retains a thread-owned termination deferral through commit or rollback.
753 plan.reset();
754 context->completed += 1;
755 return 0;
756}
757
758bool discardPrefixUsable(Cache& cache, uintptr_t key, uintptr_t expected) {
759 const uintptr_t lookup = cache.lookup(key);
760 const bool pinned = cache.pin(key);
761 bool unchanged = lookup && lookup == expected && pinned;
762 if (lookup) {
763 const auto* bytes = reinterpret_cast<const uint8_t*>(lookup);
764 for (size_t n = 0; n < PageSize; ++n) {
765 unchanged = unchanged && bytes[n] == static_cast<uint8_t>(n ^ 0x6d);
766 }
767 cache.release(key);
768 }
769 if (pinned) {
770 cache.release(key);
771 }
772 return unchanged;
773}
774
775bool preparedDiscardPublication(bool cancel) {
776 constexpr uintptr_t PrefixKey = 0xCB000000;
777 constexpr uintptr_t Key = PrefixKey + PageSize;
778 const char* test = cancel ? "cache-discard-cancel" : "cache-discard-prepublication";
779 DiscardPublicationContext context(cancel);
780 auto& publication = context.publication;
781 Cache cache;
782 publication.cache = &cache;
783 publication.key = Key;
784 cache.setCallback(retireQueuedCallback, &publication);
785 cache.startAtomic();
786 const uintptr_t prefix = cache.insert(PrefixKey);
787 publication.page = cache.insert(Key);
788 bool ownerPinned = publication.page && cache.pin(Key);
789 if (!prefix || !ownerPinned) {
790 if (ownerPinned) {
791 cache.release(Key);
792 }
793 publication.allowCallbackReturn.release();
794 cache.empty();
795 cache.endAtomic();
796 return checkNamed(false, test, "could not create and pin the test cache pages");
797 }
798 for (size_t n = 0; n < PageSize; ++n) {
799 reinterpret_cast<uint8_t*>(prefix)[n] = static_cast<uint8_t>(n ^ 0x6d);
800 reinterpret_cast<uint8_t*>(publication.page)[n] = static_cast<uint8_t>(n ^ 0xb2);
801 }
802 cache.markNoLongerEditing(PrefixKey);
803 cache.markNoLongerEditing(Key);
804 cache.markDirty(Key);
805 cache.setWritebackAdmissionHookForTest(retireAdmissionHook, &publication);
806 Thread* producer = new Thread(Scheduler::instance().getKernelProcess(), publishRetireWriteback,
807 &publication, nullptr, false, true);
808 producer->setName("hosted Cache discard writeback producer");
809 if (!publication.admissionEntered.acquire(1, 2)) {
810 publication.allowPublication.release();
811 publication.allowCallbackReturn.release();
812 producer->join();
813 cache.setWritebackAdmissionHookForTest(nullptr, nullptr);
814 cache.release(Key);
815 cache.empty();
816 cache.endAtomic();
817 return checkNamed(false, test, "writeback did not pause after publishing its page pin");
818 }
819
820 Thread* preparer = new Thread(Scheduler::instance().getKernelProcess(), preparePublishedDiscard,
821 &context, nullptr, false, true);
822 preparer->setName("hosted Cache prepared discard");
823 const bool drainPublished = waitUntilQueuedAt(preparer, Thread::CallbackDrain, Key);
824 const bool blockedBeforePublication = publication.retireReturned == 0;
825 const uintptr_t unexpectedLookup = drainPublished ? cache.lookup(Key) : 0;
826 const bool unexpectedPin = drainPublished && cache.pin(Key);
827 const bool syncRejected = drainPublished && !cache.sync(Key, true);
828 const bool admissionRejected =
829 syncRejected && !unexpectedLookup && !unexpectedPin && publication.admissionCalls == 1;
830 if (unexpectedLookup) {
831 cache.release(Key);
832 }
833 if (unexpectedPin) {
834 cache.release(Key);
835 }
836 const bool prefixDuringDrain = discardPrefixUsable(cache, PrefixKey, prefix);
837 publication.allowPublication.release();
838 const bool callbackEntered = publication.callbackEntered.acquire(1, 2);
839 const bool blockedThroughCallback = callbackEntered &&
840 waitUntilQueuedAt(preparer, Thread::CallbackDrain, Key) &&
841 publication.retireReturned == 0;
842 publication.allowCallbackReturn.release();
843 const bool readyWithOwnerPin = context.prepared.acquire(1, 2) &&
844 publication.retireReturned == 1 &&
845 publication.retireSucceeded == 1;
846 if (readyWithOwnerPin) {
847 reinterpret_cast<uint8_t*>(publication.page)[0] = 0xa7;
848 cache.markDirty(Key);
849 }
850 if (!cancel || !readyWithOwnerPin) {
851 cache.release(Key);
852 ownerPinned = false;
853 }
854 context.allowCompletion.release();
855 const bool producerJoined = producer->join();
856 const bool preparerJoined = preparer->join();
857 cache.setWritebackAdmissionHookForTest(nullptr, nullptr);
858
859 bool completedCorrectly = false;
860 if (cancel && readyWithOwnerPin) {
861 const uintptr_t restored = cache.lookup(Key);
862 const bool pinRestored = cache.pin(Key);
863 completedCorrectly = restored == publication.page && pinRestored &&
864 reinterpret_cast<const uint8_t*>(publication.page)[0] == 0xa7 &&
865 publication.queuedCallbacks == 1 && publication.evictionCalls == 0;
866 if (restored) {
867 cache.release(Key);
868 }
869 if (pinRestored) {
870 cache.release(Key);
871 }
872 cache.release(Key);
873 ownerPinned = false;
874 const bool evicted = cache.evict(Key);
875 completedCorrectly = completedCorrectly && evicted && publication.queuedCallbacks == 2 &&
876 publication.evictionCalls == 1;
877 } else if (!cancel) {
878 completedCorrectly = publication.queuedCallbacks == 1 && publication.evictionCalls == 1;
879 }
880 const bool suffixRemoved = !cache.exists(Key, PageSize);
881 const bool prefixAfterCompletion = discardPrefixUsable(cache, PrefixKey, prefix);
882 const bool passed =
883 checkNamed(drainPublished && blockedBeforePublication, test,
884 "prepare did not wait at its exact CallbackDrain key before queue publication") &&
885 checkNamed(admissionRejected && prefixDuringDrain, test,
886 "draining suffix accepted a new consumer or blocked the retained prefix") &&
887 checkNamed(blockedThroughCallback, test, "prepare returned before queued writeback ended") &&
888 checkNamed(readyWithOwnerPin, test, "prepare did not become Ready with its owner pin held") &&
889 checkNamed(producerJoined && preparerJoined && context.completed == 1 &&
890 publication.syncReturned == 1 && publication.queuedCallbackFinished == 1,
891 test, "producer or prepared-discard worker failed to complete exactly once") &&
892 checkNamed(completedCorrectly && suffixRemoved, test,
893 cancel ? "rollback lost admission or dirty data needed by ordinary eviction"
894 : "discard wrote dirty bytes back or failed to evict exactly once") &&
895 checkNamed(prefixAfterCompletion, test, "completion changed or removed the retained prefix");
896 if (ownerPinned) {
897 cache.release(Key);
898 }
899 cache.empty();
900 cache.endAtomic();
901 if (passed) {
902 NOTICE("HOSTED-WAIT-TEST: PASS " << test);
903 }
904 return passed;
905}
906
907struct RejectedWritebackContext {
908 RejectedWritebackContext()
909 : publication(),
910 producerFinished(0),
911 shutdownFinished(0),
912 accepted(0),
913 shutdownSucceeded(0) {}
914
915 RetirePublicationContext publication;
916 Semaphore producerFinished;
917 Semaphore shutdownFinished;
918 Atomic<size_t> accepted;
919 Atomic<size_t> shutdownSucceeded;
920};
921
922int publishRejectedWriteback(void* parameter) {
923 auto* context = reinterpret_cast<RejectedWritebackContext*>(parameter);
924 auto& publication = context->publication;
925 context->accepted = publication.cache->sync(publication.key, true) ? 1 : 0;
926 publication.syncReturned += 1;
927 context->producerFinished.release();
928 return 0;
929}
930
931int shutdownRejectedWriteback(void* parameter) {
932 auto* context = reinterpret_cast<RejectedWritebackContext*>(parameter);
933 context->shutdownSucceeded = context->publication.cache->shutdown() ? 1 : 0;
934 context->shutdownFinished.release();
935 return 0;
936}
937
938bool rejectedLastWritebackPin() {
939 constexpr uintptr_t Key = 0xCC000000;
940 constexpr const char* Test = "cache-rejected-last-writeback";
941 RejectedWritebackContext context;
942 auto& publication = context.publication;
943 Cache cache;
944 CacheManager& manager = CacheManager::instance();
945 publication.cache = &cache;
946 publication.key = Key;
947 cache.setCallback(retireQueuedCallback, &publication);
948 // Keep timer publication quiesced through this cache's terminal shutdown.
949 cache.startAtomic();
950 publication.page = cache.insert(Key);
951 if (!checkNamed(publication.page != 0, Test, "could not create the test page")) {
952 publication.allowCallbackReturn.release();
953 return false;
954 }
955 reinterpret_cast<uint8_t*>(publication.page)[0] = 0xa7;
956 cache.markNoLongerEditing(Key);
957 cache.markDirty(Key);
958 cache.setWritebackAdmissionHookForTest(retireAdmissionHook, &publication);
959 Thread* producer = new Thread(Scheduler::instance().getKernelProcess(), publishRejectedWriteback,
960 &context, nullptr, false, true);
961 producer->setName("hosted Cache rejected writeback producer");
962 const bool admissionPaused = publication.admissionEntered.acquire(1, 2);
963 if (admissionPaused) {
964 cache.release(Key);
965 }
966 const bool halted = admissionPaused && manager.halt();
967 const bool stopped =
968 halted && manager.getLifecycleState() == RequestQueue::LifecycleState::Stopped;
969 publication.allowCallbackReturn.release();
970 publication.allowPublication.release();
971 const bool producerFinished = context.producerFinished.acquire(1, 2);
972 if (!producerFinished) {
973 const bool resumed = manager.resume();
974 (void)resumed;
975 checkNamed(false, Test, "rejected writeback producer did not finish");
976 FATAL("Cache rejection fixture retained a live producer");
977 }
978 const bool producerJoined = producer->join();
979 const bool rejected = context.accepted == 0 && publication.syncReturned == 1 &&
980 publication.queuedCallbacks == 0 && publication.evictionCalls == 0;
981 cache.setWritebackAdmissionHookForTest(nullptr, nullptr);
982 const bool resumed = manager.resume();
983 if (!resumed && (!manager.halt() || !manager.resume())) {
984 checkNamed(false, Test, "could not restore the CacheManager worker");
985 FATAL("Cache rejection fixture could not resume CacheManager");
986 }
987
988 Thread* shutdown = new Thread(Scheduler::instance().getKernelProcess(), shutdownRejectedWriteback,
989 &context, nullptr, false, true);
990 shutdown->setName("hosted Cache rejected-writeback shutdown");
991 if (!context.shutdownFinished.acquire(1, 2)) {
992 checkNamed(false, Test, "shutdown did not drain the rejected request's cache lease");
993 FATAL("Cache rejection fixture retained a live shutdown worker");
994 }
995 const bool shutdownJoined = shutdown->join();
997 reinterpret_cast<void*>(publication.page));
998 const bool passed =
999 checkNamed(admissionPaused && stopped, Test,
1000 "writeback was not paused with its last pin before manager halt") &&
1001 checkNamed(producerFinished && producerJoined && rejected, Test,
1002 "the stopped queue executed or retained the rejected writeback") &&
1003 checkNamed(resumed, Test, "the CacheManager worker did not resume") &&
1004 checkNamed(shutdownJoined && context.shutdownSucceeded == 1 &&
1005 publication.queuedCallbacks == 1 && publication.evictionCalls == 1 && unmapped,
1006 Test, "terminal shutdown did not write back and remove the abandoned page");
1007 if (passed) {
1008 NOTICE("HOSTED-WAIT-TEST: PASS " << Test);
1009 }
1010 return passed;
1011}
1012
1013struct RetireContractContext {
1014 RetireContractContext()
1015 : cache(nullptr),
1016 key(0),
1017 page(0),
1018 shouldSucceed(0),
1019 callbacks(0),
1020 argumentsValid(0),
1021 retireReturned(0),
1022 retireSucceeded(0) {}
1023
1024 Cache* cache;
1025 uintptr_t key;
1026 uintptr_t page;
1027 Atomic<size_t> shouldSucceed;
1028 Atomic<size_t> callbacks;
1029 Atomic<size_t> argumentsValid;
1030 Atomic<size_t> retireReturned;
1031 Atomic<size_t> retireSucceeded;
1032};
1033
1034bool retireContractCallback(uintptr_t key, uintptr_t page, void* parameter) {
1035 RetireContractContext* context = reinterpret_cast<RetireContractContext*>(parameter);
1036 context->callbacks += 1;
1037 context->argumentsValid = key == context->key && page == context->page;
1038 return static_cast<size_t>(context->shouldSucceed) != 0;
1039}
1040
1041int retirePinnedWriteback(void* parameter) {
1042 RetireContractContext* context = reinterpret_cast<RetireContractContext*>(parameter);
1043 if (context->cache->retireWriteback(context->key, retireContractCallback, context)) {
1044 context->retireSucceeded += 1;
1045 }
1046 context->retireReturned += 1;
1047 return 0;
1048}
1049
1050bool retireWritebackContract() {
1051 constexpr uintptr_t EditingKey = 0xCA7E600;
1052 constexpr uintptr_t RetryKey = 0xCA7E700;
1053 constexpr uintptr_t PinnedKey = 0xCA7E800;
1054 constexpr uintptr_t MissingKey = 0xCA7E900;
1055 Cache cache;
1056
1057 RetireContractContext editing;
1058 editing.cache = &cache;
1059 editing.key = EditingKey;
1060 editing.page = cache.insert(EditingKey);
1061 editing.shouldSucceed = 1;
1062 const bool editingRejected =
1063 editing.page && !cache.retireWriteback(EditingKey, retireContractCallback, &editing) &&
1064 editing.callbacks == 0 && cache.exists(EditingKey, PageSize);
1065 const bool editingDiscarded = editingRejected && cache.discardEditing(EditingKey);
1066
1067 RetireContractContext retry;
1068 retry.cache = &cache;
1069 retry.key = RetryKey;
1070 retry.page = cache.insert(RetryKey);
1071 if (retry.page) {
1072 cache.markNoLongerEditing(RetryKey);
1073 }
1074 const bool failureKeptPage = retry.page &&
1075 !cache.retireWriteback(RetryKey, retireContractCallback, &retry) &&
1076 retry.callbacks == 1 && cache.lookup(RetryKey) == retry.page;
1077 if (failureKeptPage) {
1078 cache.release(RetryKey);
1079 }
1080 retry.shouldSucceed = 1;
1081 const bool retryRetired =
1082 failureKeptPage && cache.retireWriteback(RetryKey, retireContractCallback, &retry) &&
1083 retry.callbacks == 2 && retry.argumentsValid == 1 && !cache.exists(RetryKey, PageSize);
1084
1085 RetireContractContext pinned;
1086 pinned.cache = &cache;
1087 pinned.key = PinnedKey;
1088 pinned.page = cache.insert(PinnedKey);
1089 if (pinned.page) {
1090 cache.markNoLongerEditing(PinnedKey);
1091 }
1092 pinned.shouldSucceed = 1;
1093 const bool pinnedReady = pinned.page && cache.pin(PinnedKey);
1094 Thread* retirer = nullptr;
1095 if (pinnedReady) {
1096 retirer = new Thread(Scheduler::instance().getKernelProcess(), retirePinnedWriteback, &pinned,
1097 nullptr, false, true);
1098 retirer->setName("hosted Cache pinned-page retirer");
1099 }
1100 const bool pinDrainPublished =
1101 retirer && waitUntilQueuedAt(retirer, Thread::CallbackDrain, PinnedKey);
1102 const uintptr_t unexpectedLookup = pinDrainPublished ? cache.lookup(PinnedKey) : 0;
1103 const bool unexpectedPin = pinDrainPublished && cache.pin(PinnedKey);
1104 const bool newConsumersRejected =
1105 pinDrainPublished && pinned.retireReturned == 0 && !unexpectedLookup && !unexpectedPin;
1106 if (unexpectedLookup) {
1107 cache.release(PinnedKey);
1108 }
1109 if (unexpectedPin) {
1110 cache.release(PinnedKey);
1111 }
1112 if (pinnedReady) {
1113 cache.release(PinnedKey);
1114 }
1115 const bool pinnedJoined = retirer && retirer->join();
1116 const bool pinnedRetired = pinnedJoined && pinned.retireReturned == 1 &&
1117 pinned.retireSucceeded == 1 && pinned.callbacks == 1 &&
1118 pinned.argumentsValid == 1 && !cache.exists(PinnedKey, PageSize);
1119
1120 RetireContractContext missing;
1121 missing.cache = &cache;
1122 missing.key = MissingKey;
1123 missing.shouldSucceed = 1;
1124 const bool missingSucceeded =
1125 cache.retireWriteback(MissingKey, retireContractCallback, &missing) && missing.callbacks == 0;
1126
1127 const bool passed =
1128 checkNamed(editingDiscarded, "cache-retire-contract",
1129 "retirement invoked writeback for an Editing page") &&
1130 checkNamed(retryRetired, "cache-retire-contract",
1131 "failed writeback did not preserve a retryable page") &&
1132 checkNamed(pinDrainPublished && newConsumersRejected && pinnedRetired,
1133 "cache-retire-contract",
1134 "retirement did not drain the old pin while rejecting new consumers") &&
1135 checkNamed(missingSucceeded, "cache-retire-contract",
1136 "retiring a missing page invoked the callback or failed");
1137
1138 cache.empty();
1139 if (passed) {
1140 NOTICE("HOSTED-WAIT-TEST: PASS cache-retire-contract");
1141 }
1142 return passed;
1143}
1144
1145struct SyncAllContext {
1146 SyncAllContext()
1147 : cache(nullptr),
1148 lower(nullptr),
1149 entered(0),
1150 allowReturn(0),
1151 writes(0),
1152 blockWriteback(false),
1153 retirementSucceeds(false),
1154 nestedSucceeded(0) {}
1155
1156 Cache* cache;
1157 Cache* lower;
1158 Semaphore entered;
1159 Semaphore allowReturn;
1160 Atomic<size_t> writes;
1161 bool blockWriteback;
1162 bool retirementSucceeds;
1163 Atomic<size_t> nestedSucceeded;
1164};
1165
1166bool syncAllCallback(CacheConstants::CallbackCause cause, uintptr_t, uintptr_t, void* parameter) {
1167 auto* context = static_cast<SyncAllContext*>(parameter);
1168 if (cause != CacheConstants::WriteBack) {
1169 return true;
1170 }
1171 const size_t write = (context->writes += 1);
1172 if (context->blockWriteback && write == 1) {
1173 context->entered.release();
1174 const bool released = context->allowReturn.acquireForCompletion();
1175 (void)released;
1176 }
1177 if (context->lower) {
1178 const bool succeeded = context->lower->syncAll();
1179 context->nestedSucceeded = succeeded ? 1 : 0;
1180 return succeeded;
1181 }
1182 return true;
1183}
1184
1185bool syncAllRetirementCallback(uintptr_t, uintptr_t, void* parameter) {
1186 auto* context = static_cast<SyncAllContext*>(parameter);
1187 context->entered.release();
1188 const bool released = context->allowReturn.acquireForCompletion();
1189 (void)released;
1190 return context->retirementSucceeds;
1191}
1192
1193struct SyncAllCall {
1194 enum Kind { All, QueuedPage, Retire };
1195 SyncAllCall(SyncAllContext& state, uintptr_t cacheKey, Kind operation)
1196 : context(state), key(cacheKey), kind(operation), done(0), result(0) {}
1197 SyncAllContext& context;
1198 uintptr_t key;
1199 Kind kind;
1200 Semaphore done;
1201 Atomic<size_t> result;
1202};
1203
1204int syncAllWorker(void* parameter) {
1205 auto* call = static_cast<SyncAllCall*>(parameter);
1206 bool succeeded = false;
1207 if (call->kind == SyncAllCall::All) {
1208 succeeded = call->context.cache->syncAll();
1209 } else if (call->kind == SyncAllCall::QueuedPage) {
1210 succeeded = call->context.cache->sync(call->key, false);
1211 } else {
1212 succeeded =
1213 call->context.cache->retireWriteback(call->key, syncAllRetirementCallback, &call->context);
1214 }
1215 call->result = succeeded ? 1 : 0;
1216 call->done.release();
1217 return 0;
1218}
1219
1220bool syncAllJoinsCallback() {
1221 constexpr uintptr_t Key = 0xCA7EA00;
1222 SyncAllContext context;
1223 Cache cache;
1224 context.cache = &cache;
1225 context.blockWriteback = true;
1226 cache.setCallback(syncAllCallback, &context);
1227 if (!cache.insert(Key)) {
1228 return false;
1229 }
1230 cache.markNoLongerEditing(Key);
1231 cache.markDirty(Key);
1232 SyncAllCall first(context, Key, SyncAllCall::All);
1233 Thread* writer = new Thread(Scheduler::instance().getKernelProcess(), syncAllWorker, &first,
1234 nullptr, false, true);
1235 const bool entered = context.entered.acquire(1, 2);
1236 if (!entered) {
1237 context.allowReturn.release();
1238 writer->join();
1239 return checkNamed(false, "cache-sync-all", "direct callback did not start");
1240 }
1241
1242 SyncAllCall queued(context, Key, SyncAllCall::QueuedPage);
1243 Thread* producer = new Thread(Scheduler::instance().getKernelProcess(), syncAllWorker, &queued,
1244 nullptr, false, true);
1245 const bool queueCompleted = queued.done.acquire(1, 2);
1246 const bool queueRejected = queueCompleted && queued.result == 0;
1247 SyncAllCall second(context, Key, SyncAllCall::All);
1248 Thread* joiner = new Thread(Scheduler::instance().getKernelProcess(), syncAllWorker, &second,
1249 nullptr, false, true);
1250 const bool joinedCallback = waitUntilQueuedAt(joiner, Thread::CallbackDrain, Key);
1251 context.allowReturn.release();
1252 const bool writerJoined = writer->join();
1253 const bool producerJoined = producer->join();
1254 const bool joinerJoined = joiner->join();
1255 const bool passed =
1256 checkNamed(queueRejected && producerJoined, "cache-sync-all",
1257 "the CacheManager worker blocked behind a direct callback") &&
1258 checkNamed(joinedCallback && writerJoined && joinerJoined && first.result == 1 &&
1259 second.result == 1 && context.writes == 1,
1260 "cache-sync-all", "synchronous drain did not join an active callback");
1261 if (passed) {
1262 NOTICE("HOSTED-WAIT-TEST: PASS cache-sync-all-callback");
1263 }
1264 return passed;
1265}
1266
1267bool syncAllJoinsRetirement(bool succeeds) {
1268 constexpr uintptr_t Key = 0xCA7EB00;
1269 SyncAllContext context;
1270 Cache cache;
1271 context.cache = &cache;
1272 context.retirementSucceeds = succeeds;
1273 cache.setCallback(syncAllCallback, &context);
1274 if (!cache.insert(Key)) {
1275 return false;
1276 }
1277 cache.markNoLongerEditing(Key);
1278 cache.markDirty(Key);
1279 SyncAllCall retirement(context, Key, SyncAllCall::Retire);
1280 Thread* retirer = new Thread(Scheduler::instance().getKernelProcess(), syncAllWorker, &retirement,
1281 nullptr, false, true);
1282 const bool entered = context.entered.acquire(1, 2);
1283 if (!entered) {
1284 context.allowReturn.release();
1285 retirer->join();
1286 return checkNamed(false, "cache-sync-all", "retirement callback did not start");
1287 }
1288 SyncAllCall sync(context, Key, SyncAllCall::All);
1289 Thread* joiner = new Thread(Scheduler::instance().getKernelProcess(), syncAllWorker, &sync,
1290 nullptr, false, true);
1291 const bool joinedRetirement = waitUntilQueuedAt(joiner, Thread::CallbackDrain, Key);
1292 context.allowReturn.release();
1293 const bool retirerJoined = retirer->join();
1294 const bool joinerJoined = joiner->join();
1295 const bool passed = checkNamed(
1296 joinedRetirement && retirerJoined && joinerJoined && sync.result == 1 &&
1297 retirement.result == (succeeds ? 1U : 0U) && context.writes == (succeeds ? 0U : 1U) &&
1298 cache.exists(Key, PageSize) != succeeds,
1299 "cache-sync-all", "drain did not join retirement or retry its retained failure");
1300 if (passed) {
1301 NOTICE("HOSTED-WAIT-TEST: PASS cache-sync-all-retirement-" << (succeeds ? "success" : "retry"));
1302 }
1303 return passed;
1304}
1305
1306bool syncAllFromCacheManager() {
1307 constexpr uintptr_t Key = 0xCA7EC00;
1308 SyncAllContext lowerContext;
1309 Cache lower;
1310 lowerContext.cache = &lower;
1311 lower.setCallback(syncAllCallback, &lowerContext);
1312 SyncAllContext upperContext;
1313 Cache upper;
1314 upperContext.cache = &upper;
1315 upperContext.lower = &lower;
1316 upper.setCallback(syncAllCallback, &upperContext);
1317 if (!lower.insert(Key) || !upper.insert(Key)) {
1318 return false;
1319 }
1320 lower.markNoLongerEditing(Key);
1321 upper.markNoLongerEditing(Key);
1322 lower.markDirty(Key);
1323 upper.markDirty(Key);
1324 const bool passed = checkNamed(
1325 upper.sync(Key, false) && upperContext.nestedSucceeded == 1 && lowerContext.writes == 1,
1326 "cache-sync-all", "a CacheManager callback could not drain an independent lower cache");
1327 if (passed) {
1328 NOTICE("HOSTED-WAIT-TEST: PASS cache-sync-all-nested");
1329 }
1330 return passed;
1331}
1332
1333bool rangeExistence() {
1334 constexpr uintptr_t Key = 0xCA7E500;
1335 constexpr size_t Length = 3 * PageSize;
1336 constexpr uintptr_t ProbeKey = Key + (8 * PageSize);
1337 constexpr uintptr_t SecondProbeKey = ProbeKey + (4 * PageSize);
1338 Cache cache;
1339
1340 // Reserve and return a known six-page allocator extent. After a rejected
1341 // overlap, the same extent must still split into the same two halves.
1342 // The old partial-publication path leaked the skipped virtual page, which
1343 // forced the second half to be allocated elsewhere.
1344 const uintptr_t allocatorExtent = cache.insert(ProbeKey, 2 * Length);
1345 const bool allocatorProbeReady = allocatorExtent != 0;
1346 cache.empty();
1347
1348 const uintptr_t pages = cache.insert(Key, Length);
1349 const bool completeRange = pages != 0 && cache.exists(Key, Length);
1350 bool alreadyExisted = false;
1351 const uintptr_t reused = cache.insert(Key, Length, &alreadyExisted);
1352 const bool completeRangeReused = alreadyExisted && reused == pages;
1353 const bool removedInterior = cache.discardEditing(Key + PageSize);
1354 const bool missingInteriorRejected = removedInterior && !cache.exists(Key, Length);
1355 cache.empty();
1356
1357 const uintptr_t interior = cache.insert(Key + PageSize);
1358 bool overlapExisted = true;
1359 const uintptr_t overlappingRange = cache.insert(Key, Length, &overlapExisted);
1360 const bool overlapRejectedBeforeAllocation =
1361 interior != 0 && !overlappingRange && !overlapExisted &&
1362 cache.exists(Key + PageSize, PageSize) && !cache.exists(Key, PageSize) &&
1363 !cache.exists(Key + (2 * PageSize), PageSize) && !cache.exists(Key, Length);
1364 cache.empty();
1365
1366 const uintptr_t firstHalf = cache.insert(ProbeKey, Length);
1367 const uintptr_t secondHalf = cache.insert(SecondProbeKey, Length);
1368 const bool allocatorAndPageAccountingBalanced =
1369 allocatorProbeReady && firstHalf == allocatorExtent &&
1370 secondHalf == allocatorExtent + Length && cache.exists(ProbeKey, Length) &&
1371 cache.exists(SecondProbeKey, Length);
1372 cache.empty();
1373
1374 const bool passed = checkNamed(completeRange && completeRangeReused, "cache-range-existence",
1375 "a complete contiguous cache range was not detected or reused") &&
1376 checkNamed(missingInteriorRejected, "cache-range-existence",
1377 "a range with a missing interior page was reported complete") &&
1378 checkNamed(overlapRejectedBeforeAllocation, "cache-range-existence",
1379 "an interior overlap partially allocated or published a range") &&
1380 checkNamed(allocatorAndPageAccountingBalanced, "cache-range-existence",
1381 "a rejected overlap leaked cache VA or page accounting");
1382 if (passed) {
1383 NOTICE("HOSTED-WAIT-TEST: PASS cache-range-existence");
1384 }
1385 return passed;
1386}
1387
1388bool strictRangeGeometry() {
1389 constexpr uintptr_t InsertKey = 0xCA7F000;
1390 constexpr uintptr_t PublishKey = InsertKey + (4 * PageSize);
1391 Cache cache;
1392
1393 bool alreadyExisted = true;
1394 const uintptr_t invalid = cache.insert(InsertKey, PageSize + 1, &alreadyExisted);
1395 const bool insertionRejected = !invalid && !alreadyExisted && !cache.exists(InsertKey, PageSize);
1396
1397 const uintptr_t editing = cache.insert(InsertKey);
1398 cache.markNoLongerEditing(InsertKey, PageSize + 1);
1399 const bool invalidPublishLeftEditing = editing && cache.discardEditing(InsertKey);
1400
1401 const uintptr_t published = cache.insert(PublishKey);
1402 cache.markNoLongerEditing(PublishKey);
1403 cache.markEditing(PublishKey, PageSize + 1);
1404 const bool invalidEditLeftPublished = published && !cache.discardEditing(PublishKey);
1405
1406 cache.empty();
1407 const bool passed =
1408 checkNamed(insertionRejected, "cache-range-geometry",
1409 "a partial target-page insertion was truncated instead of rejected") &&
1410 checkNamed(invalidPublishLeftEditing, "cache-range-geometry",
1411 "an invalid publish range changed the first cache page") &&
1412 checkNamed(invalidEditLeftPublished, "cache-range-geometry",
1413 "an invalid edit range changed the first cache page");
1414 if (passed) {
1415 NOTICE("HOSTED-WAIT-TEST: PASS cache-range-geometry");
1416 }
1417 return passed;
1418}
1419
1420struct TimerWritebackContext {
1421 Cache* cache = nullptr;
1422 uintptr_t key = 0;
1423 Semaphore admissionEntered{0};
1424 Semaphore allowPublication{0};
1425 Semaphore callbackEntered{0};
1426 Semaphore allowCallbackReturn{0};
1427 Atomic<size_t> admissions{0};
1428 Atomic<size_t> callbacks{0};
1429 uint8_t lastWritten = 0;
1430 bool failFirst = false;
1431};
1432
1433void timerWritebackAdmission(Cache*, uintptr_t, void* parameter) {
1434 auto& context = *static_cast<TimerWritebackContext*>(parameter);
1435 if ((context.admissions += 1) == 1) {
1436 context.cache->startAtomic();
1437 context.admissionEntered.release();
1438 const bool released = context.allowPublication.acquireForCompletion();
1439 (void)released;
1440 }
1441}
1442
1443bool timerWritebackCallback(CacheConstants::CallbackCause cause, uintptr_t, uintptr_t page,
1444 void* parameter) {
1445 if (cause != CacheConstants::WriteBack) {
1446 return true;
1447 }
1448 auto& context = *static_cast<TimerWritebackContext*>(parameter);
1449 const size_t call = (context.callbacks += 1);
1450 context.lastWritten = *reinterpret_cast<uint8_t*>(page);
1451 if (call == 1) {
1452 context.callbackEntered.release();
1453 const bool released = context.allowCallbackReturn.acquireForCompletion();
1454 (void)released;
1455 }
1456 return !(context.failFirst && call == 1);
1457}
1458
1459void tickWriteback(Cache& cache) {
1460 cache.endAtomic();
1461 cache.timer(CACHE_WRITEBACK_PERIOD * 1000000ULL);
1462 cache.startAtomic();
1463}
1464
1465int publishTimerWriteback(void* parameter) {
1466 auto& context = *static_cast<TimerWritebackContext*>(parameter);
1467 tickWriteback(*context.cache);
1468 return 0;
1469}
1470
1471bool timerWritebackCoalescing(bool failFirst, bool mutateDuringWriteback) {
1472 const char* test = failFirst ? "cache-timer-pending-failure"
1473 : (mutateDuringWriteback ? "cache-timer-pending-mutation"
1474 : "cache-timer-pending-success");
1475 TimerWritebackContext context;
1476 context.failFirst = failFirst;
1477 context.key = 0xCA7F800;
1478 Cache cache;
1479 context.cache = &cache;
1480 cache.startAtomic();
1481 cache.setCallback(timerWritebackCallback, &context);
1482 const uintptr_t page = cache.insert(context.key);
1483 if (!checkNamed(page != 0, test, "could not create the test page")) {
1484 return false;
1485 }
1486 *reinterpret_cast<uint8_t*>(page) = 0x57;
1487 cache.markNoLongerEditing(context.key);
1488 tickWriteback(cache);
1489 cache.markDirty(context.key);
1490 cache.setWritebackAdmissionHookForTest(timerWritebackAdmission, &context);
1491
1492 // A separate synchronous request fences the worker after each released
1493 // callback, including checksum publication and writeback-pin retirement.
1494 Cache fence;
1495 fence.startAtomic();
1496 fence.setCallback([](CacheConstants::CallbackCause, uintptr_t, uintptr_t, void*) { return true; },
1497 nullptr);
1498 const uintptr_t fencePage = fence.insert(0);
1499 if (!checkNamed(fencePage != 0, test, "could not create the worker fence")) {
1500 cache.setWritebackAdmissionHookForTest(nullptr, nullptr);
1501 context.allowCallbackReturn.release();
1502 return false;
1503 }
1504 fence.markNoLongerEditing(0);
1505
1506 Thread* producer = new Thread(Scheduler::instance().getKernelProcess(), publishTimerWriteback,
1507 &context, nullptr, false, true);
1508 producer->setName("hosted Cache paused timer producer");
1509 const bool admissionPaused = context.admissionEntered.acquire(1, 2);
1510 if (admissionPaused) {
1511 for (size_t i = 0; i < 3; ++i) {
1512 tickWriteback(cache);
1513 }
1514 }
1515 const bool onePendingAdmission = context.admissions == 1;
1516 context.allowPublication.release();
1517 const bool producerJoined = producer->join();
1518 const bool callbackPaused = context.callbackEntered.acquire(1, 2);
1519 if (callbackPaused) {
1520 for (size_t i = 0; i < 3; ++i) {
1521 tickWriteback(cache);
1522 }
1523 if (mutateDuringWriteback) {
1524 *reinterpret_cast<uint8_t*>(page) = 0xA6;
1525 }
1526 }
1527 const bool oneActiveAdmission = context.admissions == 1;
1528 context.allowCallbackReturn.release();
1529 fence.markDirty(0);
1530 const bool firstDrained = fence.sync(0, false);
1531 const bool oneInitialCallback = context.callbacks == 1;
1532
1533 // Failure is immediately retryable; a mutation must first be detected by
1534 // the checksum scan. Drain each epoch so queued work cannot mask a retry.
1535 bool retriesDrained = true;
1536 for (size_t i = 0; i < 3; ++i) {
1537 tickWriteback(cache);
1538 fence.markDirty(0);
1539 retriesDrained = fence.sync(0, false) && retriesDrained;
1540 }
1541 const size_t expected = failFirst || mutateDuringWriteback ? 2 : 1;
1542 const bool expectedCallbacks = context.callbacks == expected && context.admissions == expected;
1543 const bool expectedBytes = context.lastWritten == (mutateDuringWriteback ? 0xA6 : 0x57);
1544 cache.setWritebackAdmissionHookForTest(nullptr, nullptr);
1545 const bool reclaimed = cache.empty();
1546
1547 const bool passed =
1548 checkNamed(admissionPaused && callbackPaused && producerJoined, test,
1549 "writeback did not reach both controlled publication phases") &&
1550 checkNamed(onePendingAdmission && oneActiveAdmission, test,
1551 "timer admitted duplicate work while writeback was pending or active") &&
1552 checkNamed(firstDrained && oneInitialCallback && retriesDrained, test,
1553 "worker did not drain exactly one initial writeback") &&
1554 checkNamed(expectedCallbacks && expectedBytes, test,
1555 "completion lost a mutation, failed to retry, or wrote clean data again") &&
1556 checkNamed(reclaimed, test, "completed writeback retained a page pin");
1557 if (passed) {
1558 NOTICE("HOSTED-WAIT-TEST: PASS " << test);
1559 }
1560 return passed;
1561}
1562
1563struct ExplicitCacheCall {
1564 enum Kind { Sync, Lookup, Redirty };
1565 ExplicitCacheCall(TimerWritebackContext& state, Kind operation)
1566 : context(state), kind(operation) {}
1567 TimerWritebackContext& context;
1568 Kind kind;
1569 uintptr_t page = 0;
1570 Semaphore done{0};
1571 Atomic<size_t> result{0};
1572};
1573
1574int explicitCacheWorker(void* parameter) {
1575 auto& call = *static_cast<ExplicitCacheCall*>(parameter);
1576 Cache& cache = *call.context.cache;
1577 const uintptr_t key = call.context.key;
1578 bool succeeded = false;
1579 if (call.kind == ExplicitCacheCall::Sync) {
1580 succeeded = cache.syncAll();
1581 } else if (call.kind == ExplicitCacheCall::Lookup) {
1582 // Transfer the lookup pin to the test so it can verify eviction refusal.
1583 succeeded = cache.lookupStable(key, call.page, true);
1584 } else {
1585 call.page = cache.lookup(key);
1586 if (call.page) {
1587 *reinterpret_cast<uint8_t*>(call.page) = 0xA6;
1588 cache.markDirty(key);
1589 cache.release(key);
1590 succeeded = true;
1591 }
1592 }
1593 call.result = succeeded ? 1 : 0;
1594 call.done.release();
1595 return 0;
1596}
1597
1598bool explicitWritebackThreading(bool redirty) {
1599 const char* test = redirty ? "cache-explicit-concurrent-redirty" : "cache-explicit-lookup-wakeup";
1600 TimerWritebackContext context;
1601 context.key = 0xCA7F900;
1602 Cache cache;
1603 context.cache = &cache;
1604 cache.startAtomic();
1605 cache.setDirtyTracking(Cache::DirtyTracking::Explicit);
1606 cache.setCallback(timerWritebackCallback, &context);
1607 const uintptr_t page = cache.insert(context.key);
1608 if (!checkNamed(page != 0, test, "could not create the test page")) {
1609 return false;
1610 }
1611 *reinterpret_cast<uint8_t*>(page) = 0x57;
1612 cache.markNoLongerEditing(context.key);
1613 cache.markDirty(context.key);
1614
1615 ExplicitCacheCall first(context, ExplicitCacheCall::Sync);
1616 Thread* writer = new Thread(Scheduler::instance().getKernelProcess(), explicitCacheWorker, &first,
1617 nullptr, false, true);
1618 writer->setName("hosted Cache explicit paused writer");
1619 const bool callbackPaused = context.callbackEntered.acquire(1, 2);
1620 if (!callbackPaused) {
1621 context.allowCallbackReturn.release();
1622 writer->joinForCompletion();
1623 return checkNamed(false, test, "explicit dirty callback did not start");
1624 }
1625
1626 ExplicitCacheCall second(context,
1627 redirty ? ExplicitCacheCall::Redirty : ExplicitCacheCall::Lookup);
1628 Thread* peer = new Thread(Scheduler::instance().getKernelProcess(), explicitCacheWorker, &second,
1629 nullptr, false, true);
1630 peer->setName(
1631 String(redirty ? "hosted Cache concurrent explicit mutation" : "hosted Cache stable lookup"));
1632 const bool concurrentPhase = redirty
1633 ? second.done.acquire(1, 2)
1634 : waitUntilQueuedAt(peer, Thread::CallbackDrain, context.key);
1635 context.allowCallbackReturn.release();
1636 const bool writerJoined = writer->joinForCompletion();
1637 bool peerCompleted = concurrentPhase;
1638 if (!redirty) {
1639 peerCompleted = second.done.acquire(1, 2);
1640 if (!peerCompleted) {
1641 // A lost completion wake must fail the test without stranding its worker.
1642 // The cache and page remain alive, and the writer has left the callback.
1643 Thread::WaitDebugInfo wait = {};
1644 uintptr_t address = 0;
1645 if (peer->getWaitDebugInfo(wait) && wait.queue && wait.queued &&
1646 peer->getDebugState(address) == Thread::CallbackDrain && address == context.key) {
1647 wait.queue->wakeAll(WaitQueue::WakeReason::Signalled,
1648 WaitQueue::Channel(wait.channelOwner, wait.channelValue));
1649 }
1650 }
1651 }
1652 const bool peerJoined = peer->joinForCompletion();
1653 const bool firstWrite =
1654 first.result == 1 && context.callbacks == 1 && context.lastWritten == 0x57;
1655 const bool samePage = second.result == 1 && second.page == page;
1656 bool lookupPinned = true;
1657 if (!redirty) {
1658 lookupPinned = samePage && !cache.evict(context.key);
1659 if (second.page) {
1660 cache.release(context.key);
1661 }
1662 }
1663
1664 const bool nextSynced = cache.sync(context.key, false);
1665 const size_t expectedWrites = redirty ? 2 : 1;
1666 const bool latestWritten =
1667 context.callbacks == expectedWrites && context.lastWritten == (redirty ? 0xA6 : 0x57);
1668 const bool cleanSynced = cache.syncAll();
1669 const bool stayedClean = context.callbacks == expectedWrites;
1670 const bool reclaimed = cache.empty();
1671 const bool passed =
1672 checkNamed(concurrentPhase && peerCompleted && writerJoined && peerJoined, test,
1673 redirty ? "mutation did not finish while the callback was paused"
1674 : "stable lookup did not publish and complete its callback-drain wait") &&
1675 checkNamed(firstWrite && samePage && lookupPinned, test,
1676 "initial write or the concurrent page pin changed identity") &&
1677 checkNamed(nextSynced && latestWritten && cleanSynced && stayedClean, test,
1678 "callback completion lost a dirty generation or rewrote a clean page") &&
1679 checkNamed(reclaimed, test, "completed workers retained a page pin");
1680 if (passed) {
1681 NOTICE("HOSTED-WAIT-TEST: PASS " << test);
1682 }
1683 return passed;
1684}
1685
1686struct DirectWritebackContext {
1687 Cache* cache = nullptr;
1688 uintptr_t key = 0;
1689 uintptr_t page = 0;
1690 Semaphore entered{0};
1691 Semaphore allowReturn{0};
1692 Atomic<size_t> callbacks{0};
1693 Atomic<size_t> direct{0};
1694 Atomic<size_t> observed{0};
1695 Atomic<size_t> syncResult{0};
1696 Atomic<size_t> writerResult{0};
1697};
1698
1699bool directWritebackCallback(const Cache::WritebackPage* pages, size_t count, void* parameter) {
1700 auto& context = *static_cast<DirectWritebackContext*>(parameter);
1701 if (count != 1 || pages[0].key != context.key) {
1702 return false;
1703 }
1705 if (!lease.acquire(*context.cache, pages[0].key, pages[0].location)) {
1706 return false;
1707 }
1708 context.direct = lease.physical() != 0;
1709 const size_t call = context.callbacks += 1;
1710 if (call == 1) {
1711 context.entered.release();
1712 const bool released = context.allowReturn.acquireForCompletion();
1713 (void)released;
1714 }
1715 context.observed = *reinterpret_cast<const uint8_t*>(pages[0].location);
1716 return true;
1717}
1718
1719int directWritebackSync(void* parameter) {
1720 auto& context = *static_cast<DirectWritebackContext*>(parameter);
1721 context.syncResult =
1722 context.cache->syncBatch(&context.key, 1, directWritebackCallback, &context) ? 1 : 0;
1723 return 0;
1724}
1725
1726int directWritebackWriter(void* parameter) {
1727 auto& context = *static_cast<DirectWritebackContext*>(parameter);
1728 if (!context.cache->pin(context.key)) {
1729 return 0;
1730 }
1731 if (context.cache->beginMutableLoan(context.key)) {
1732 *reinterpret_cast<uint8_t*>(context.page) = 0xA6;
1733 context.cache->markDirty(context.key);
1734 context.cache->endMutableLoan(context.key);
1735 context.writerResult = 1;
1736 }
1737 context.cache->release(context.key);
1738 return 0;
1739}
1740
1741bool directWritebackLoan() {
1742 constexpr const char* Test = "cache-direct-writeback-loan";
1743 DirectWritebackContext context;
1745 context.cache = &cache;
1746 context.key = 0xCA7FA00;
1747 cache.setDirtyTracking(Cache::DirtyTracking::Explicit);
1748 cache.setCallback([](CacheConstants::CallbackCause, uintptr_t, uintptr_t, void*) { return true; },
1749 nullptr);
1750 context.page = cache.insert(context.key);
1751 if (!checkNamed(context.page != 0, Test, "could not create the test page")) {
1752 return false;
1753 }
1754 *reinterpret_cast<uint8_t*>(context.page) = 0x57;
1755 cache.markNoLongerEditing(context.key);
1756 cache.markDirty(context.key);
1757
1758 Thread* sync = new Thread(Scheduler::instance().getKernelProcess(), directWritebackSync, &context,
1759 nullptr, false, true);
1760 sync->setName("hosted Cache direct writeback");
1761 const bool entered = context.entered.acquire(1, 2);
1762 Thread* writer = nullptr;
1763 bool queued = false;
1764 bool frozen = false;
1765 if (entered) {
1766 writer = new Thread(Scheduler::instance().getKernelProcess(), directWritebackWriter, &context,
1767 nullptr, false, true);
1768 writer->setName("hosted Cache direct writeback writer");
1769 queued = waitUntilQueuedAt(writer, Thread::CallbackDrain, context.key);
1770 frozen = *reinterpret_cast<const uint8_t*>(context.page) == 0x57;
1771 }
1772 context.allowReturn.release();
1773 const bool synced = sync->joinForCompletion();
1774 const bool writerJoined = !writer || writer->joinForCompletion();
1775 const bool first = context.syncResult == 1 && context.direct == 1 && context.observed == 0x57 &&
1776 context.writerResult == 1;
1777 const bool second = cache.syncBatch(&context.key, 1, directWritebackCallback, &context) &&
1778 context.callbacks == 2 && context.observed == 0xA6;
1779 const bool reclaimed = cache.empty();
1780 const bool passed =
1781 checkNamed(entered && queued && frozen && synced && writerJoined, Test,
1782 "writer did not wait for the immutable DMA lease") &&
1783 checkNamed(first && second, Test, "writeback lost the old or new page contents") &&
1784 checkNamed(reclaimed, Test, "test page retained a reference");
1785 if (passed) {
1786 NOTICE("HOSTED-WAIT-TEST: PASS " << Test);
1787 }
1788 return passed;
1789}
1790} // namespace
1791
1792bool runHostedCacheDiscardRegressions() {
1793 return preparedDiscardPublication(false) && preparedDiscardPublication(true) &&
1794 rejectedLastWritebackPin();
1795}
1796
1797bool runHostedCacheSyncRegressions() {
1798 return syncAllJoinsCallback() && syncAllJoinsRetirement(true) && syncAllJoinsRetirement(false) &&
1799 syncAllFromCacheManager();
1800}
1801
1802bool runHostedCacheDirectWritebackRegressions() {
1803 return directWritebackLoan();
1804}
1805
1806bool runHostedCacheTimerRegressions() {
1807 return cacheManagerTrimWakeGating() && timerWritebackCoalescing(false, false) &&
1808 timerWritebackCoalescing(true, false) && timerWritebackCoalescing(false, true);
1809}
1810
1811bool runHostedCacheRegressions() {
1812 return callbackLifetime() && queuedRequestLifetime() && emptyAndReuse() &&
1813 retirementPublication() && failedPublicationDiscard() && retirePrepublicationWriteback() &&
1814 runHostedCacheDiscardRegressions() && retireWritebackContract() && rangeExistence() &&
1815 strictRangeGeometry() && runHostedCacheSyncRegressions() &&
1816 runHostedCacheTimerRegressions() && explicitWritebackThreading(false) &&
1817 explicitWritebackThreading(true) && directWritebackLoan();
1818}
bool m_bActive
Definition Cache.h:199
Definition Cache.h:207
void setCallback(writeback_t newCallback, void *meta)
Definition Cache.cc:2390
uintptr_t insert(uintptr_t key, bool *alreadyExisted=nullptr)
Definition Cache.cc:890
void startAtomic()
Definition Cache.h:594
void markDirty(uintptr_t key)
Definition Cache.cc:2135
bool sync(uintptr_t key, bool async)
Definition Cache.cc:1627
void markNoLongerEditing(uintptr_t key, size_t length=0)
Definition Cache.cc:2646
LifecycleState getLifecycleState()
MUST_USE_RESULT bool halt()
MUST_USE_RESULT bool resume()
static Scheduler & instance()
Definition Scheduler.h:96
void yield()
Definition Scheduler.cc:236
static constexpr size_t getPageSize() noexcept
Definition TargetInfo.h:40
bool getWaitDebugInfo(WaitDebugInfo &info)
Definition Thread.cc:3163
bool joinForCompletion()
Definition Thread.cc:2750
bool join()
Definition Thread.cc:2746
DebugState getDebugState(uintptr_t &address)
Definition Thread.h:574
virtual bool isMapped(void *virtualAddress)=0
static EXPORTED_PUBLIC VirtualAddressSpace & getKernelAddressSpace()