Skip to content

[Performance] Bound scheduler service under persistent runnable work #1028

Description

@Coldwings

Feature Description

Add an internal scheduler service quantum so a worker periodically services
external submissions and ready I/O even while it remains continuously runnable.

Problem Statement

worker_thread::get_next_task() currently pops the owner-local Chase-Lev deque
first and drains the MPSC inbox only after that deque becomes empty. Likewise,
poll_io_when_idle() is reached only when no runnable task exists. A coroutine
that repeatedly yields can therefore keep the local deque non-empty and defer
both a remote submission and an already-ready I/O completion for the full
duration of that runnable backlog.

The existing scheduler_service_benchmark reproduces this deterministically:
with a 5 ms local yield backlog on current main, both service probes complete
only after roughly the whole backlog interval (about 5.07 ms in local smoke
runs). This is scheduler service starvation, not eventfd syscall latency.

Proposed Solution

Reuse the worker's existing count of completed main-loop task dispatches and,
at fixed internal quanta:

  1. recheck and drain external submissions before choosing another local task;
  2. if the worker I/O context has pending operations, perform one non-blocking
    poll(0ms) to deliver already-ready completions; and
  3. keep the service schedule independent of whether work came from the local
    deque or was stolen.

Paired measurements selected an external-inbox quantum of 256 completed
worker-loop task dispatches and a pending-I/O quantum of 16,384 dispatches.
Keep this as an internal policy in this change; no public scheduler
configuration is required.

API Design

No public API change.

The enforceable contract is a cooperative worker-loop dispatch-count bound:
once user code returns to the scheduler, persistent runnable work cannot
suppress a service opportunity for more than the selected number of further
main-loop dispatches. It is not a wall-clock latency guarantee because one
coroutine dispatch may run arbitrary user code before suspending. A backend
poll may also synchronously deliver a batch of completion resumptions before
control returns to the worker loop; those inline resumes do not consume the
dispatch budget.

Alternatives Considered

Service only when the local deque becomes empty

This is the current behavior and permits unbounded starvation under a
persistent yield/reschedule loop.

Poll I/O on every task resume

This minimizes service delay but adds a backend syscall whenever long-lived I/O
is pending, imposing excessive overhead on CPU-heavy runnable workloads.

Wall-clock-only polling

Reading the clock and enforcing a time interval can cap cooperative wall-clock
delay more directly, but it complicates the hot path and still cannot constrain
non-yielding user code. A small task-count quantum is simpler and deterministic;
time-based adaptation can be considered separately if measurements justify it.

Publicly configurable quantum

This exposes scheduler policy and compatibility obligations before a stable
default has been established. Benchmark and ship an internal default first.

Use Cases

  1. A single worker serving sockets while CPU-side coroutines remain runnable.
  2. Cross-thread task submission to a worker running a local yield loop.
  3. Latency-sensitive timers or poll completions sharing a worker with sustained
    cooperative computation.

Implementation Considerations

  • Complexity: Medium; the service point interacts with local LIFO order,
    inbox publication, backend completion resumption, affinity, and shutdown.
  • Breaking Changes: No.
  • Dependencies: None.
  • Performance Impact: Expected large service-latency reduction with a small,
    explicitly measured throughput cost. The CPU-only path should not make I/O
    syscalls when the worker has no pending operations.
  • Preserve owner-local continuation routing, work stealing, affinity, active I/O
    pinning, resize/drain behavior, and exactly-once resumption.
  • Cover epoll and io_uring; non-blocking polls must have equivalent scheduling
    semantics across both backends.

Compatibility

  • C++ Standard: C++20, unchanged.
  • Platform: Linux, unchanged.
  • Backward Compatibility: Public API and task ownership are unchanged; only
    scheduling fairness under sustained local runnable work improves.

Related Work

Acceptance Criteria

  • Add deterministic regression coverage proving persistent local runnable work
    cannot indefinitely starve a remote inbox task or ready I/O completion.
  • Exercise both epoll and io_uring where io_uring is available.
  • Report paired scheduler_service_benchmark latency and CPU-only
    reschedule/yield throughput for the selected quantum versus main.
  • Run the full unit suite plus scheduler-focused ASAN and TSAN validation.
  • Document the cooperative service-bound semantics and benchmark trade-off.

Checklist

  • I have searched existing issues to avoid duplicates
  • I have clearly described the problem and proposed solution
  • I have considered alternative approaches
  • I have provided usage examples
  • I have considered backward compatibility

Priority

  • High: Would significantly improve my workflow

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions