Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 91 additions & 42 deletions dm/host/tests/t_pollwait.c
Original file line number Diff line number Diff line change
@@ -1,64 +1,113 @@
// Short timed waits as polls (poll_wait_begin / poll_wait_end in hle_thread.c), through real thread SVCs on one guest thread:
// 1. a loop of condition variable waits of 500 ns that time out (Hyrule Warriors: Definitive Edition's update thread, main+0x2ad224) backs off after 2 ms;
// 2. so does a loop of 30 ns waits and SleepThread(1 µs) in turn (its main thread, main+0x2acdc0), on WaitSynchronization as well;
// 3. a loop with more than SPIN_GAP_NS of work between its waits never does;
// 4. a lengthened wait still returns as soon as it is signalled.
#include "../hle_thread.c"
// Short timed waits as polls, through real thread SVCs on one guest thread.
// Drive the run/gap clock explicitly: a CI deschedule longer than SPIN_GAP_NS
// legitimately resets the run, so 20 ms of wall time need not include a backoff.
// Host wait deadlines still use the real clock and pthread timed waits.
#include "../dm.h"
#include <assert.h>
#include <stdatomic.h>
#include <time.h>
#include <unistd.h>

static _Atomic uint64_t poll_clock;
static uint64_t test_now_ns(void) { return atomic_load(&poll_clock); }
static _Thread_local struct timespec wait_start;
static int test_clock_gettime(clockid_t clock, struct timespec *ts) {
int r = clock_gettime(clock, ts);
if (!r && clock == CLOCK_REALTIME) wait_start = *ts;
return r;
}
static int signal_wait, waiter_ready; // signal_wait is set before starting the signaller; waiter_ready is protected by G
static pthread_cond_t ready_cv = PTHREAD_COND_INITIALIZER;
static int test_timedwait(pthread_cond_t *cv, pthread_mutex_t *mutex, const struct timespec *deadline) {
if (!signal_wait) return pthread_cond_timedwait(cv, mutex, deadline);
int64_t ns = (deadline->tv_sec - wait_start.tv_sec) * 1000000000ll + deadline->tv_nsec - wait_start.tv_nsec;
assert(ns == 250000); // the production wait must have been lengthened, not merely counted as a backoff
waiter_ready = 1; pthread_cond_signal(&ready_cv);
// Only the signal case has no OS timeout: the signaller must run after this
// waiter releases G, even when CI takes longer than 250 us to schedule it.
// The real condition wait and SignalProcessWideKey still perform the wake.
return pthread_cond_wait(cv, mutex);
}
#define now_ns test_now_ns
#define clock_gettime test_clock_gettime
#define pthread_cond_timedwait test_timedwait
#include "../hle_thread.c"
#undef now_ns
#undef clock_gettime
#undef pthread_cond_timedwait

static _Atomic uint32_t mutex_word, condition_key;
static uint32_t me_handle;
static uint64_t backoffs(void) { thr_t *me = pthread_getspecific(g_keythr); assert(me); return me->nbackoff; }
static void spin_ns(uint64_t ns) { uint64_t t = now_ns(); while (now_ns() - t < ns) {} }
// One condition variable wait as the SDK's TimedWait makes it: take the lock in user mode, wait (the timeout releases it without taking it back)
static int64_t cv_wait(int64_t ns) {
atomic_store(&mutex_word, me_handle);
uint64_t r[32] = {(uint64_t)&mutex_word, (uint64_t)&condition_key, me_handle, (uint64_t)ns};
svc_thread(0x1C, r);
if (r[0] == 0) { r[0] = (uint64_t)&mutex_word; svc_thread(0x1B, r); } // signalled: the waiter holds the lock again
return (int64_t)r[0];
int64_t result = (int64_t)r[0];
if (!result) {
assert(atomic_load(&mutex_word) == me_handle); // a signalled waiter owns the lock again
}
return result;
}
static void poll_step(int64_t wait_ns, int sleep_too, uint32_t event) {
if (event) { uint64_t r[32] = {0, (uint64_t)&event, 1, (uint64_t)wait_ns}; svc_thread(0x18, r); assert(r[0] == R_TIMEOUT); }
else assert(cv_wait(wait_ns) == R_TIMEOUT);
if (sleep_too) { uint64_t r[32] = {1000}; svc_thread(0x0B, r); }
}
static void sleep_svc(int64_t ns) { uint64_t r[32] = {(uint64_t)ns}; svc_thread(0x0B, r); }
// Passes of a poll for ms milliseconds; → the back-off steps taken (yield sleeps and lengthened waits)
static uint64_t poll_for(int ms, int64_t wait_ns, int sleep_too, uint64_t work_ns, uint32_t event) {
sleep_svc(1000000); // a real sleep ends the previous case's run
uint64_t b0 = backoffs(), t0 = now_ns();
while (now_ns() - t0 < (uint64_t)ms * 1000000) {
if (event) { uint64_t r[32] = {0, (uint64_t)&event, 1, (uint64_t)wait_ns}; svc_thread(0x18, r); assert(r[0] == R_TIMEOUT); }
else assert(cv_wait(wait_ns) == R_TIMEOUT);
if (sleep_too) sleep_svc(1000);
if (work_ns) spin_ns(work_ns);
static void poll_run(int64_t wait_ns, int sleep_too, uint32_t event) {
atomic_fetch_add(&poll_clock, 30000); // guest work ends the previous run
uint64_t b0 = backoffs();
for (int i = 0; i < 200; i++) { // 0 through 1.99 ms: no backoff yet
poll_step(wait_ns, sleep_too, event);
assert(backoffs() == b0);
atomic_fetch_add(&poll_clock, 10000);
}
return backoffs() - b0;
poll_step(wait_ns, sleep_too, event); // exactly 2 ms: the wait (and any yield) backs off
assert(backoffs() == b0 + 1 + sleep_too);
}
static _Atomic int woken;
static void *signaller(void *a) { // signals every 50 µs until the waiter is woken (a signal while it is between two waits wakes nobody)
usleep(*(unsigned *)a);
while (!atomic_load(&woken)) { uint64_t r[32] = {(uint64_t)&condition_key, 1}; svc_thread(0x1D, r); usleep(50); }
static void *signaller(void *unused) {
(void)unused;
pthread_mutex_lock(&G);
while (!waiter_ready) pthread_cond_wait(&ready_cv, &G);
assert(g_wait && g_wait->wkind == 2 && g_wait->wkey == (uint64_t)&condition_key);
pthread_mutex_unlock(&G);
uint64_t r[32] = {(uint64_t)&condition_key, 1}; svc_thread(0x1D, r);
return NULL;
}
int main(void) {
alarm(20);
me_handle = svc_thread_main_init();
int ok = 1;
uint64_t b = poll_for(20, 500, 0, 0, 0);
printf("%s: 500 ns condition variable waits alone back off after 2 ms (%llu steps in 20 ms)\n", b ? "PASS" : "FAIL", (unsigned long long)b); ok &= b > 0;
b = poll_for(20, 30, 1, 0, 0);
printf("%s: 30 ns waits between 1 µs sleeps back off (%llu steps in 20 ms)\n", b ? "PASS" : "FAIL", (unsigned long long)b); ok &= b > 0;
poll_run(500, 0, 0);
puts("PASS: 500 ns condition variable waits back off at 2 ms, not before");
poll_run(30, 1, 0);
puts("PASS: 30 ns waits between 1 us sleeps back off at 2 ms, not before");
uint64_t r[32] = {0}; svc_thread(0x45, r); assert(r[0] == 0); uint32_t ev_w = (uint32_t)r[1], ev_r = (uint32_t)r[2];
b = poll_for(20, 30, 1, 0, ev_r);
printf("%s: the same with WaitSynchronization (%llu steps in 20 ms)\n", b ? "PASS" : "FAIL", (unsigned long long)b); ok &= b > 0;
b = poll_for(20, 500, 0, 3 * SPIN_GAP_NS / 2, 0);
printf("%s: a loop that works %d µs between its waits does not back off (%llu steps)\n", !b ? "PASS" : "FAIL", 3 * SPIN_GAP_NS / 2000, (unsigned long long)b); ok &= !b;
// a wait lengthened to the back-off's sleep ends at the signal: the poll runs into the back-off, then another thread signals during a wait
poll_for(5, 500, 0, 0, 0);
unsigned delay = 100; pthread_t s; pthread_create(&s, NULL, signaller, &delay);
uint64_t t0 = now_ns(); int64_t res = R_TIMEOUT;
while (res == R_TIMEOUT && now_ns() - t0 < 50000000) res = cv_wait(500);
atomic_store(&woken, 1); pthread_join(s, NULL);
uint64_t dt = now_ns() - t0;
printf("%s: a lengthened wait returns at the signal (%s after %.0f µs)\n", res == 0 ? "PASS" : "FAIL", res == 0 ? "signalled" : "not signalled", dt / 1e3); ok &= res == 0;
poll_run(30, 1, ev_r);
puts("PASS: the same with WaitSynchronization");

uint64_t b0 = backoffs();
atomic_fetch_add(&poll_clock, 20000); poll_step(500, 0, 0);
assert(backoffs() == b0 + 1);
for (int i = 0; i < 200; i++) { atomic_fetch_add(&poll_clock, 20001); poll_step(500, 0, 0); }
assert(backoffs() == b0 + 1);
puts("PASS: a 20 us gap keeps the run; longer gaps reset it");

poll_run(500, 0, 0);
b0 = backoffs();
r[0] = 0; r[1] = me_handle; svc_thread(0x0C, r); assert(r[0] == 0);
poll_step(500, 0, 0); assert(backoffs() == b0);
puts("PASS: an intervening non-poll SVC resets the run");

poll_run(500, 0, 0);
b0 = backoffs();
signal_wait = 1;
pthread_t s; assert(!pthread_create(&s, NULL, signaller, NULL));
assert(cv_wait(500) == 0);
assert(!pthread_join(s, NULL)); signal_wait = 0;
assert(backoffs() == b0 + 1);
poll_step(500, 0, 0); assert(backoffs() == b0 + 1);
puts("PASS: a lengthened wait returns at the signal and the next poll starts a new run");
r[0] = ev_w; svc_thread(0x16, r); r[0] = ev_r; svc_thread(0x16, r);
return ok ? 0 : 1;
return 0;
}