@samitouri / QOSamiQemu / commits / 9563d0b5e2

aio-poll: refine iothread polling using weighted handler intervals

Improve adaptive polling by updating each AioHandler's poll.ns every loop iteration using weighted averages. This reduces CPU consumption while minimizing performance impact. Background: Starting from QEMU 10.0, poll.ns was introduced per event handler to mitigate excessive fluctuations in IOThread polling times observed in earlier versions (QEMU 9.x). However, the current design has limitations: 1. poll.ns is updated only when an event occurs, making it difficult to treat block_ns as a reliable event interval. 2. The IOThread's next polling time is determined by the maximum poll.ns among all AioHandlers, meaning idle AioHandlers with high poll.ns can have an outsized impact on polling duration. 3. For io_uring, idle AioHandlers are cleared after POLL_IDLE_INTERVAL_NS (7s), but for ppoll/epoll there is no such mechanism, leading to increased CPU consumption from idle nodes. Implementation: This patch treats block_ns as an event interval and updates each AioHandler's poll.ns in every loop iteration: - Active handlers (with events): poll.ns is updated using a weighted average of the current block_ns and previous poll.ns, smoothing out adjustments and preventing excessive fluctuations. - Inactive handlers (no events): poll.ns accumulates block_ns without weighting, allowing rapid isolation of idle nodes. When poll.ns exceeds poll_max_ns, it resets to 0, preventing sporadically active handlers from unnecessarily prolonging iothread polling. - The iothread polling duration is set based on the largest poll.ns among active handlers. The shrink divider defaults to 2, matching the grow rate, to reduce frequent poll_ns resets for slow devices. The implementation renames poll_idle_timeout to last_dispatch_timestamp for use as an active handler identifier. Testing: POLL_WEIGHT_SHIFT=3 (12.5% weight) was selected based on testing comparing baseline vs weight=2/3 across various workloads: Performance results (RHEL 10.1 + QEMU 10.0.0, FCP/FICON, 1-8 iothreads, numjobs 1/4/8 averaged): | poll-weight=2 | poll-weight=3 --------------------|--------------------|----------------- Throughput avg | -2.4% (all tests) | -2.2% (all tests) CPU consumption avg | -10.9% (all tests) | -9.4% (all tests) Both configurations achieve ~10% CPU reduction with minimal throughput impact (~2%). Weight=3 is chosen as default for slightly better throughput while maintaining substantial CPU savings. Additional validation testing on s390x SSD with fio (bs=8k, iodepth=8, numjobs=1) shows how poll_weight affects polling time (poll.ns) behavior: RandRead workload: +-------------+-----------+-----------+-------------+-------------+ | poll_weight | #samples | Mean (ns) | 50th % (ns) | 90th % (ns) | +-------------+-----------+-----------+-------------+-------------+ | 1 | 4.79M | 8,034 | 5,116 | 20,509 | | 2 | 5.01M | 12,584 | 11,078 | 24,693 | | 3 | 5.01M | 15,647 | 14,863 | 28,695 | | 4 | 5.12M | 16,430 | 15,556 | 30,848 | | 5 | 5.14M | 16,461 | 15,306 | 32,123 | +-------------+-----------+-----------+-------------+-------------+ RandWrite workload: +-------------+-----------+-----------+-------------+-------------+ | poll_weight | #samples | Mean (ns) | 50th % (ns) | 90th % (ns) | +-------------+-----------+-----------+-------------+-------------+ | 1 | 6.37M | 2,049 | 1,262 | 4,301 | | 2 | 7.46M | 4,118 | 3,226 | 7,476 | | 3 | 7.97M | 7,034 | 5,984 | 11,645 | | 4 | 7.96M | 12,789 | 11,362 | 20,040 | | 5 | 7.82M | 22,992 | 20,644 | 32,768 | +-------------+-----------+-----------+-------------+-------------+ Signed-off-by: Jaehoon Kim <jhkim@linux.ibm.com> Message-ID: <20260423195918.661299-3-jhkim@linux.ibm.com> Signed-off-by: Stefan Hajnoczi <stefanha@redhat.com>

Jaehoon Kim committed Apr 23, 2026 at 14:59 UTC 9563d0b5e2d2056373e8af9eeb674fe64c19b2b2
4 files changed +90 -46
include/qemu/aio.h
+2 -1
@@ -195,7 +195,7 @@ struct BHListSlice {
195 typedef QSLIST_HEAD(, AioHandler) AioHandlerSList;
196
197 typedef struct AioPolledEvent {
198 - int64_t ns; /* current polling time in nanoseconds */
198 + int64_t ns; /* estimated block time in nanoseconds */
199 } AioPolledEvent;
200
201 struct AioContext {
@@ -306,6 +306,7 @@ struct AioContext {
306 int poll_disable_cnt;
307
308 /* Polling mode parameters */
309 + int64_t poll_ns; /* current polling time in nanoseconds */
310 int64_t poll_max_ns; /* maximum polling time in nanoseconds */
311 int64_t poll_grow; /* polling time growth factor */
312 int64_t poll_shrink; /* polling time shrink factor */
util/aio-posix.c
+86 -44
@@ -29,9 +29,11 @@
29
30 /* Stop userspace polling on a handler if it isn't active for some time */
31 #define POLL_IDLE_INTERVAL_NS (7 * NANOSECONDS_PER_SECOND)
32 +#define POLL_WEIGHT_SHIFT (3)
33
33 -static void adjust_polling_time(AioContext *ctx, AioPolledEvent *poll,
34 - int64_t block_ns);
34 +static void update_handler_poll_times(AioContext *ctx, int64_t block_ns,
35 + int64_t dispatch_time);
36 +static void adjust_polling_time(AioContext *ctx, int64_t block_ns);
37
38 bool aio_poll_disabled(AioContext *ctx)
39 {
@@ -359,7 +361,7 @@ static bool aio_dispatch_handler(AioContext *ctx, AioHandler *node)
361
362 static bool aio_dispatch_ready_handlers(AioContext *ctx,
363 AioHandlerList *ready_list,
362 - int64_t block_ns)
364 + int64_t dispatch_time)
365 {
366 bool progress = false;
367 AioHandler *node;
@@ -369,11 +371,11 @@ static bool aio_dispatch_ready_handlers(AioContext *ctx,
371 progress = aio_dispatch_handler(ctx, node) || progress;
372
373 /*
372 - * Adjust polling time only after aio_dispatch_handler(), which can
373 - * add the handler to ctx->poll_aio_handlers.
374 + * Update last_dispatch_timestamp to mark this as an active
375 + * handler for polling time adjustment and prevent idle removal.
376 */
377 if (ctx->poll_max_ns && QLIST_IS_INSERTED(node, node_poll)) {
376 - adjust_polling_time(ctx, &node->poll, block_ns);
378 + node->last_dispatch_timestamp = dispatch_time;
379 }
380 }
381
@@ -394,7 +396,7 @@ void aio_dispatch(AioContext *ctx)
396 ctx->fdmon_ops->dispatch(ctx);
397 }
398
397 - /* block_ns is 0 because polling is disabled in the glib event loop */
399 + /* Set now to 0 as polling is disabled in the glib event loop */
400 aio_dispatch_ready_handlers(ctx, &ready_list, 0);
401
402 aio_free_deleted_handlers(ctx);
@@ -415,9 +417,6 @@ static bool run_poll_handlers_once(AioContext *ctx,
417 QLIST_FOREACH_SAFE(node, &ctx->poll_aio_handlers, node_poll, tmp) {
418 if (node->io_poll(node->opaque)) {
419 aio_add_poll_ready_handler(ready_list, node);
418 -
419 - node->poll_idle_timeout = now + POLL_IDLE_INTERVAL_NS;
420 -
420 /*
421 * Polling was successful, exit try_poll_mode immediately
422 * to adjust the next polling time.
@@ -458,11 +457,10 @@ static bool remove_idle_poll_handlers(AioContext *ctx,
457 }
458
459 QLIST_FOREACH_SAFE(node, &ctx->poll_aio_handlers, node_poll, tmp) {
461 - if (node->poll_idle_timeout == 0LL) {
462 - node->poll_idle_timeout = now + POLL_IDLE_INTERVAL_NS;
463 - } else if (now >= node->poll_idle_timeout) {
460 + if (node->poll_ready == false &&
461 + now >= node->last_dispatch_timestamp + POLL_IDLE_INTERVAL_NS) {
462 trace_poll_remove(ctx, node, node->pfd.fd);
465 - node->poll_idle_timeout = 0LL;
463 + node->last_dispatch_timestamp = 0LL;
464 QLIST_SAFE_REMOVE(node, node_poll);
465 if (ctx->poll_started && node->io_poll_end) {
466 node->io_poll_end(node->opaque);
@@ -560,18 +558,13 @@ static bool run_poll_handlers(AioContext *ctx, AioHandlerList *ready_list,
558 static bool try_poll_mode(AioContext *ctx, AioHandlerList *ready_list,
559 int64_t *timeout)
560 {
563 - AioHandler *node;
561 int64_t max_ns;
562
563 if (QLIST_EMPTY_RCU(&ctx->poll_aio_handlers)) {
564 return false;
565 }
566
570 - max_ns = 0;
571 - QLIST_FOREACH(node, &ctx->poll_aio_handlers, node_poll) {
572 - max_ns = MAX(max_ns, node->poll.ns);
573 - }
574 - max_ns = qemu_soonest_timeout(*timeout, max_ns);
567 + max_ns = qemu_soonest_timeout(*timeout, ctx->poll_ns);
568
569 if (max_ns && !ctx->fdmon_ops->need_wait(ctx)) {
570 /*
@@ -587,43 +580,85 @@ static bool try_poll_mode(AioContext *ctx, AioHandlerList *ready_list,
580 return false;
581 }
582
590 -static void adjust_polling_time(AioContext *ctx, AioPolledEvent *poll,
591 - int64_t block_ns)
583 +static void adjust_polling_time(AioContext *ctx, int64_t block_ns)
584 {
593 - if (block_ns <= poll->ns) {
594 - /* This is the sweet spot, no adjustment needed */
595 - } else if (block_ns > ctx->poll_max_ns) {
596 - /* We'd have to poll for too long, poll less */
597 - int64_t old = poll->ns;
598 -
599 - if (ctx->poll_shrink) {
600 - poll->ns /= ctx->poll_shrink;
601 - } else {
602 - poll->ns = 0;
585 + if (block_ns < ctx->poll_ns) {
586 + int64_t old = ctx->poll_ns;
587 + int64_t shrink = ctx->poll_shrink;
588 +
589 + if (shrink == 0) {
590 + shrink = 2;
591 + }
592 +
593 + if (block_ns < (ctx->poll_ns / shrink)) {
594 + ctx->poll_ns /= shrink;
595 }
596
605 - trace_poll_shrink(ctx, old, poll->ns);
606 - } else if (poll->ns < ctx->poll_max_ns &&
607 - block_ns < ctx->poll_max_ns) {
597 + trace_poll_shrink(ctx, old, ctx->poll_ns);
598 + } else if (block_ns > ctx->poll_ns) {
599 /* There is room to grow, poll longer */
609 - int64_t old = poll->ns;
600 + int64_t old = ctx->poll_ns;
601 int64_t grow = ctx->poll_grow;
602
603 if (grow == 0) {
604 grow = 2;
605 }
606
616 - if (poll->ns) {
617 - poll->ns *= grow;
607 + if (block_ns > ctx->poll_ns * grow) {
608 + ctx->poll_ns = block_ns;
609 } else {
619 - poll->ns = 4000; /* start polling at 4 microseconds */
610 + ctx->poll_ns *= grow;
611 }
612
622 - if (poll->ns > ctx->poll_max_ns) {
623 - poll->ns = ctx->poll_max_ns;
613 + if (ctx->poll_ns > ctx->poll_max_ns) {
614 + ctx->poll_ns = ctx->poll_max_ns;
615 }
616
626 - trace_poll_grow(ctx, old, poll->ns);
617 + trace_poll_grow(ctx, old, ctx->poll_ns);
618 + }
619 +}
620 +
621 +static void update_handler_poll_times(AioContext *ctx, int64_t block_ns,
622 + int64_t dispatch_time)
623 +{
624 + AioHandler *node;
625 + int64_t max_poll_ns = -1;
626 +
627 + QLIST_FOREACH(node, &ctx->poll_aio_handlers, node_poll) {
628 + if (node->last_dispatch_timestamp == dispatch_time) {
629 + /*
630 + * Active handler: had an event in this aio_poll() call.
631 + * Update poll.ns using a weighted average of the current
632 + * block_ns and previous poll.ns to smooth adjustments.
633 + */
634 + node->poll.ns = node->poll.ns
635 + ? (node->poll.ns - (node->poll.ns >> POLL_WEIGHT_SHIFT))
636 + + (block_ns >> POLL_WEIGHT_SHIFT) : block_ns;
637 +
638 + if (node->poll.ns > ctx->poll_max_ns) {
639 + node->poll.ns = 0;
640 + }
641 + /*
642 + * Track the maximum poll.ns among active handlers to
643 + * calculate the next polling time.
644 + */
645 + max_poll_ns = MAX(max_poll_ns, node->poll.ns);
646 + } else {
647 + /*
648 + * Inactive handler: no event in this aio_poll() call but
649 + * was active before. Increase poll.ns by block_ns. If it
650 + * exceeds poll_max_ns, reset to 0 until next event.
651 + */
652 + if (node->poll.ns != 0) {
653 + node->poll.ns += block_ns;
654 + if (node->poll.ns > ctx->poll_max_ns) {
655 + node->poll.ns = 0;
656 + }
657 + }
658 + }
659 + }
660 + if (max_poll_ns >= 0) {
661 + adjust_polling_time(ctx, max_poll_ns);
662 }
663 }
664
@@ -635,6 +670,7 @@ bool aio_poll(AioContext *ctx, bool blocking)
670 int64_t timeout;
671 int64_t start = 0;
672 int64_t block_ns = 0;
673 + int64_t dispatch_ns = 0;
674
675 /*
676 * There cannot be two concurrent aio_poll calls for the same AioContext (or
@@ -711,7 +747,8 @@ bool aio_poll(AioContext *ctx, bool blocking)
747
748 /* Calculate blocked time for adaptive polling */
749 if (ctx->poll_max_ns) {
714 - block_ns = qemu_clock_get_ns(QEMU_CLOCK_REALTIME) - start;
750 + dispatch_ns = qemu_clock_get_ns(QEMU_CLOCK_REALTIME);
751 + block_ns = dispatch_ns - start;
752 }
753
754 if (ctx->fdmon_ops->dispatch) {
@@ -719,10 +756,14 @@ bool aio_poll(AioContext *ctx, bool blocking)
756 }
757
758 progress |= aio_bh_poll(ctx);
722 - progress |= aio_dispatch_ready_handlers(ctx, &ready_list, block_ns);
759 + progress |= aio_dispatch_ready_handlers(ctx, &ready_list, dispatch_ns);
760
761 aio_free_deleted_handlers(ctx);
762
763 + if (ctx->poll_max_ns) {
764 + update_handler_poll_times(ctx, block_ns, dispatch_ns);
765 + }
766 +
767 qemu_lockcnt_dec(&ctx->list_lock);
768
769 progress |= timerlistgroup_run_timers(&ctx->tlg);
@@ -794,6 +835,7 @@ void aio_context_set_poll_params(AioContext *ctx, int64_t max_ns,
835 ctx->poll_max_ns = max_ns;
836 ctx->poll_grow = grow;
837 ctx->poll_shrink = shrink;
838 + ctx->poll_ns = 0;
839
840 aio_notify(ctx);
841 }
util/aio-posix.h
+1 -1
@@ -38,7 +38,7 @@ struct AioHandler {
38 unsigned flags; /* see fdmon-io_uring.c */
39 CqeHandler internal_cqe_handler; /* used for POLL_ADD/POLL_REMOVE */
40 #endif
41 - int64_t poll_idle_timeout; /* when to stop userspace polling */
41 + int64_t last_dispatch_timestamp; /* when last handler was dispatched */
42 bool poll_ready; /* has polling detected an event? */
43 AioPolledEvent poll;
44 };
util/async.c
+1
@@ -606,6 +606,7 @@ AioContext *aio_context_new(Error **errp)
606 timerlistgroup_init(&ctx->tlg, aio_timerlist_notify, ctx);
607
608 ctx->poll_max_ns = 0;
609 + ctx->poll_ns = 0;
610 ctx->poll_grow = 0;
611 ctx->poll_shrink = 0;
612