Skip to content

Hot Path Windows Short Waits

github-actions[bot] edited this page Sep 14, 2026 · 3 revisions

Windows Short-Wait Policy

Lifecycle: Performance - Implemented · Type: Heuristic wait shortcut
State: Present in current main.

Status

Field State at reviewed main
Underlying route Implemented as Windows/Xbox short-wait boolean
Current control Reduce CPU usage while waiting, live, default On
Planned control Issue #98: Auto / Low latency / Lower CPU use
Exact scope Non-ppoll Xbox path and 0 < timeout < 1.25 ms
Maintained reference Busy-spin to deadline, then poll original handles with zero timeout
Evidence counters Focused tests/source checks; shared #94 route artifact absent
Qualification API/fault/readiness coverage; policy tradeoff remains workload-specific
Reviewed source 322986f4c5502b3fa887a9605ac8cd88e9b2c579, 2026-09-14
Primary history Host-wait work around issues #19/#20 and PR #25; control PR #37; planned #98

Summary

For very short timeouts on the Windows non-ppoll path, current xemu historically busy-spins until the nanosecond deadline and then polls original handles with timeout zero. That preserves sub-millisecond timing but burns an active CPU core during the wait.

When the CPU-saving control is enabled, xemu first attempts a persistent thread-local high-resolution Windows waitable timer. It appends that timer to a private copy of the original GPollFD array and waits on both the timer and the original event handles. Event readiness can therefore end the wait before the timeout. Any unsupported, re-entrant, create/arm/cancel, or lifecycle condition falls back to the busy-spin reference route.

The control does not affect every qemu_poll_ns() call. Zero, negative/infinite, long, ppoll, and non-Windows paths follow their normal implementations.

Why It May Help

The primary target is host CPU/power, not necessarily lower latency:

eligible short waits
× active busy-spin duration replaced by kernel wait

On a thread that repeatedly waits hundreds of microseconds, replacing spin time can reduce active CPU cycles, heat, fan/power use, and contention with the vCPU or renderer.

The tradeoff is wake behavior. Kernel scheduling and timer delivery can add latency or tail variance compared with spinning. A title already limited by another thread may gain nothing in guest progress even while CPU use falls.

Observed in xemu

Current main has focused Windows tests for sub-millisecond return, original event readiness, enable/disable behavior, failure/backoff, and cleanup. PR #37 qualified the integrated control path but did not claim a fresh exact-head game-speed improvement.

Historical work treated the route as a CPU-use policy. This distinction is essential: fewer active cycles in the helper do not prove a faster frame, and a lower mean can coexist with worse p99 wake intervals.

External Precedent

Direct host API: Microsoft documents CreateWaitableTimerExW and CREATE_WAITABLE_TIMER_HIGH_RESOLUTION for high-resolution timer objects, with explicit handle lifetime and access requirements: CreateWaitableTimerExW.

General host-wait precedent: Windows waitable timer objects allow threads to block until a due time rather than consume active CPU: Waitable timer objects.

The API does not guarantee that this policy is best for xemu. xemu additionally has to preserve GLib poll semantics, original event readiness, nanosecond deadline rounding, re-entry safety, and per-thread lifecycle.

Emulation Boundary

QEMU/xemu main-loop or worker wait request
original event handles + nanosecond timeout
    ↓
host waiting strategy
busy-spin reference OR high-resolution timer + original handles
    ↓
required result
same ready handles, revents, return/error semantics, and no early timeout

This policy does not alter an emulated PIT, RTC, NV2A PTIMER, or other Xbox device timer. It changes how a host thread waits between emulator events.

Code and Ownership Map

Location Role Owner
util/qemu-timer.c::qemu_poll_ns() Scope gate, reference spin, normal poll paths Calling thread
XboxHighResolutionPoll TLS state Timer handle, private poll array, re-entry/backoff/lifecycle One instance per calling thread
xbox_high_resolution_poll_ns() Create/reuse, arm, wait, copy readiness, cancel/drain Calling thread
qemu_poll_set/get_cpu_saving() Current atomic boolean UI publishes; poll threads read
include/qemu/timer.h Public wait-policy API surface QEMU utility layer
issue #98 Planned typed policy/counters Not implemented at reviewed main

Actual Current Flow

flowchart TD
    A[qemu_poll_ns] --> B{CONFIG_PPOLL path?}
    B -->|Yes| C[Use ppoll; shortcut not involved]
    B -->|No| D{Xbox and 0 < timeout < 1.25 ms?}
    D -->|No| E[Convert timeout and call normal g_poll]
    D -->|Yes| F{CPU-saving control enabled?}
    F -->|No| R[Busy-spin to deadline]
    F -->|Yes| G{TLS route re-entered, unavailable or in backoff?}
    G -->|Yes| R
    G -->|No| H[Create or reuse thread-local high-resolution timer]
    H -->|Failure| R
    H --> I[Copy original poll descriptors and append timer handle]
    I --> J[Recompute remaining deadline]
    J -->|Expired| K[g_poll original descriptors with timeout 0]
    J -->|Remaining| L[Arm relative timer rounded up to 100 ns]
    L -->|Failure| R
    L --> M[Block on original handles plus timer]
    M --> N[Copy original revents/count back]
    N --> O{Original event won before timer?}
    O -->|Yes| P[Cancel and drain timer]
    O -->|No| Q[Timer represents timeout]
    P --> S[Return original-handle ready count]
    Q --> S
    R --> T[Set timeout 0 after deadline]
    T --> K
Loading

The appended timer is internal. Its revents is never reported as a caller event.

Maintained Reference Flow

flowchart TD
    A[Eligible sub-1.25 ms wait] --> B[Read realtime clock]
    B --> C[Spin until deadline]
    C --> D[Set timeout to zero]
    D --> E[g_poll original handles]
    E --> F[Return original readiness]
Loading

This is the current short-wait reference—not a generic blocking wait. Longer timeouts use normal g_poll millisecond conversion instead.

Eligibility and Protected Invariants

Invariant Why required Failure if weakened
Scope limited to eligible short waits Policy was designed for sub-ms busy-spin replacement Unexpected long/infinite wait behavior
Original handles copied and waited together Events must interrupt timeout Missed I/O/interrupt wake
Timer is thread-local Handle/array/re-entry state is not cross-thread safe Data race/corruption
Re-entry guard Alertable/re-entrant path can recurse Reuse in-flight timer/array
Deadline recomputed after setup Setup consumes part of timeout Late/early wait
Relative due time rounds up to 100 ns Host timer must not expire before requested QEMU deadline Early timeout/busy loop
Original revents copied back exactly Caller observes GLib poll contract Wrong event handling
Successful early event cancels/drains timer Timer must not remain signaled/armed for next call Spurious immediate timeout
Unknown cancellation outcome closes handle Unsafe timer cannot be reused Stale signal/race
Unsupported/failure path backs off/falls back Avoid repeated expensive failures Log/CPU thrash
errno preserved on poll error Utility API semantics Wrong caller behavior

State, Lifetime, Invalidation, and Fallback

Each thread lazily owns one timer handle and a growable private poll-descriptor array. The timer persists across eligible waits and is closed at thread exit. Permanent unsupported errors mark the route unavailable; transient failures set a retry deadline.

The control is live because the next eligible wait reads the atomic boolean. An already executing wait is not rewritten.

Fallback sentinel INT_MIN is internal: the caller then executes the established busy-spin path. A real g_poll error remains an error and preserves errno.

Expected Profile Signature

A suitable workload should show:

many 0 < timeout < 1.25 ms calls
    + high-resolution wait attempts/successes
    + lower active CPU during waits
    + original event wake counts preserved
    + unchanged guest progress/correctness

Potential tradeoff signature:

  • CPU time/use falls;
  • p95/p99/max wake or frame intervals worsen;
  • more scheduler-ready delay appears;
  • guest progress remains unchanged;
  • timer failures cause frequent fallback.

Profile scheduled running versus waiting/ready time. A sleeping thread is not automatically faster.

Counters and Evidence

Counter Unit Meaning
host.wait.calls calls All qemu_poll_ns calls
zero/negative/long_timeout_calls calls Out-of-scope classes
short_wait_eligible calls 0–1.25 ms scope reached
high_res_attempts/successes calls Optimized route admission/result
busy_spin_reference calls Short-wait reference used
event_wakes calls Original handle ended wait
timer_timeouts calls Timer ended wait
create/arm/cancel/poll_failures errors Failure class
reentry/unavailable/backoff_fallbacks calls Safety fallback reason
requested/actual wait duration ns Latency/tail evidence
active CPU and ready delay ns Resource/scheduling outcome

Counters should be per owning thread and merged during low-frequency evidence export, not one contended atomic on every wait.

Risks and Known Bugs

  • Omitting original handles turns an interruptible wait into a timer-only sleep.
  • Rounding down a 100-ns due time expires early.
  • Reusing a timer after uncertain cancellation can produce a stale signal.
  • Failing to drain an already-signaled timer can make the next wait return immediately.
  • Re-entry can corrupt TLS buffer/timer state.
  • Recreating the timer every call would replace spin CPU with kernel/object overhead.
  • Permanent unsupported hosts must not retry every wait.
  • A disconnected automation client does not prove server-side helper/trace processes stopped.
  • Lower CPU use can worsen latency tails; the correct decision can be “keep selectable,” not “always faster.”

See issue #98, PR #25, PR #37, Debugging Tools & Commands, and Pull Request Lessons.

Qualification

  1. Timeout classes: negative, zero, 1 ns, threshold-1, threshold, threshold+1, long.
  2. No-handle timeout and one/multiple original handles.
  3. Each original handle winning before timer; exact revents/ready count.
  4. Timer timeout with no original readiness.
  5. Create, arm, cancel, poll error, unsupported, retry/backoff, and re-entry injection.
  6. Thread-exit cleanup and repeated process lifecycle.
  7. On/Off same-binary fixed workload with route counters.
  8. Scheduled running/wait/ready partition, host CPU/power where available, guest progress.
  9. P50/p95/p99/max wake and frame intervals.
  10. OpenGL and Vulkan representative titles because host wait is shared.
  11. Non-Windows and ppoll source/build controls proving no accidental route change.

The planned #98 enum should preserve the old boolean migration exactly and expose fallbacks rather than pretending the requested lower-CPU route handled every eligible call.

Clone this wiki locally