All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH v2 00/12] block: BLOCK_IO_DELAY event
@ 2026-10-19 18:20 Hanna Czenczek
  2026-10-19 18:20 ` [PATCH v2 01/12] qapi/common: Clarify what IoOperationType is for Hanna Czenczek
                   ` (11 more replies)
  0 siblings, 12 replies; 18+ messages in thread
From: Hanna Czenczek @ 2026-10-19 18:20 UTC (permalink / raw)
  To: qemu-block
  Cc: qemu-devel, Hanna Czenczek, Kevin Wolf, John Snow,
	Denis V . Lunev, Eric Blake, Markus Armbruster, Stefan Hajnoczi,
	Paolo Bonzini

Hi,

v1: https://lists.nongnu.org/archive/html/qemu-block/2026-08/msg00701.html

I’m told there are installations where storage is very slow, and some
would like the VM stack to report this proactively.  To do so, we should
(via QAPI events) report on extremely slow I/O requests.

We already have the latency histogram, but this is not deemed sufficient
because it is not proactively reporting and would require repeated
querying.  Therefore, this series introduces the event still.

In contrast to v1, this version adds the delay tracking to the
BlockBackend request code instead of using the existing cookies from
block/accounting.c.

As explained in the v1 cover letter, in order to be able to raise a
BLOCK_IO_DELAY event exactly when a request crosses the threshold, we
need to keep requests in a list, which requires trivially verifiable
resource management, i.e. trivially being able to see that every request
that is begun being tracked (and thus put into that list) is also
removed from that list once the request is done.  This was not possible
with the accounting cookies, so instead, as suggested by Stefan, this
version adds its own tracking directly into the BB-level requests.

The downside is that accounting and delay tracking thus track different
kinds of requests.  If that is unacceptable, the only alternatives would
be:
- To do it with cookies, which is basically impossible, as explained,
- To change the cookies to be tied to BB-level requests as well, which
  would (A) be an invasive code change, and (B) change the existing
  reporting in an incompatible way.

Personally, I found the divergence to be more acceptable than having a
go at said invasive change, and having to defend the incompatible
reporting change.


In addition to what v1 provides, with this threshold-level reporting, we
can now also have the VM stop on exceedingly delayed requests, so the
guest would not see that delay, as a last-ditch effort for guests that
find extreme latencies to be catastrophic.  (Which is to say Windows
does that: It throws a BSOD when exceeding a certain maximum latency.)


Note patches 3 and 4 that might not really fully fit into this series;
they fix a pre-existing bug around the runstate transitions between
IO_ERROR and SUSPENDED.  An AI review tool informed me that the
transition between SUSPENDED and IO_HANG was missing in patch 5, but
given that I had just copied what IO_ERROR does, I felt bad adding that
transition for IO_HANG, but leaving IO_ERROR as it was.


git-backport-diff against v1 is not so useful here, I’m afraid:

Key:
[----] : patches are identical
[####] : number of functional differences between upstream/downstream patch
[down] : patch is downstream-only
The flags [FC] indicate (F)unctional and (C)ontextual differences, respectively

001/12:[down] 'qapi/common: Clarify what IoOperationType is for'
002/12:[0004] [FC] 'qapi/block: Add IoAccountingOperation enum'
003/12:[down] 'runstate: Allow IO_ERROR/SUSPENDED transitions'
004/12:[down] 'iotests: PC-only test for I/O errors during suspend'
005/12:[down] 'qapi/run-state: Add io-hang state'
006/12:[0017] [FC] 'qapi/block: Add BLOCK_IO_DELAY event'
007/12:[down] 'qapi/block: Add BLOCK_IO_DONE event'
008/12:[----] [--] 'block-backend: Public blk_get_attached_dev_path()'
009/12:[down] 'host-utils: Add u{add,sub}{32,64}_saturate()'
010/12:[down] 'block: Per-request delay tracker'
011/12:[down] 'block: Add delay-report-ns and delay-stop-ns props'
012/12:[down] 'iotests: Add io-delay-reporting test'


Hanna Czenczek (12):
  qapi/common: Clarify what IoOperationType is for
  qapi/block: Add IoAccountingOperation enum
  runstate: Allow IO_ERROR/SUSPENDED transitions
  iotests: PC-only test for I/O errors during suspend
  qapi/run-state: Add io-hang state
  qapi/block: Add BLOCK_IO_DELAY event
  qapi/block: Add BLOCK_IO_DONE event
  block-backend: Public blk_get_attached_dev_path()
  host-utils: Add u{add,sub}{32,64}_saturate()
  block: Per-request delay tracker
  block: Add delay-report-ns and delay-stop-ns props
  iotests: Add io-delay-reporting test

 qapi/block.json                               |  90 +++++
 qapi/common.json                              |   5 +-
 qapi/run-state.json                           |   7 +-
 include/block/block-backend-delay.h           | 112 ++++++
 include/hw/block/block.h                      |   8 +-
 include/qemu/host-utils.h                     |  62 ++++
 include/system/block-backend-global-state.h   |  11 +
 include/system/block-backend-io.h             |   9 +
 block/block-backend-delay.c                   | 326 ++++++++++++++++++
 block/block-backend.c                         |  50 ++-
 blockdev.c                                    |  16 +
 hw/block/block.c                              |   7 +
 system/runstate.c                             |  27 +-
 block/meson.build                             |   1 +
 tests/qemu-iotests/172.out                    |  76 ++++
 tests/qemu-iotests/tests/io-delay-reporting   | 187 ++++++++++
 .../qemu-iotests/tests/io-delay-reporting.out |  90 +++++
 .../tests/io-error-during-suspend             | 155 +++++++++
 .../tests/io-error-during-suspend.out         |  22 ++
 19 files changed, 1244 insertions(+), 17 deletions(-)
 create mode 100644 include/block/block-backend-delay.h
 create mode 100644 block/block-backend-delay.c
 create mode 100755 tests/qemu-iotests/tests/io-delay-reporting
 create mode 100644 tests/qemu-iotests/tests/io-delay-reporting.out
 create mode 100755 tests/qemu-iotests/tests/io-error-during-suspend
 create mode 100644 tests/qemu-iotests/tests/io-error-during-suspend.out

-- 
2.55.0



^ permalink raw reply	[flat|nested] 18+ messages in thread

* [PATCH v2 01/12] qapi/common: Clarify what IoOperationType is for
  2026-10-19 18:20 [PATCH v2 00/12] block: BLOCK_IO_DELAY event Hanna Czenczek
@ 2026-10-19 18:20 ` Hanna Czenczek
  2026-10-20  5:58   ` Markus Armbruster
  2026-10-19 18:20 ` [PATCH v2 02/12] qapi/block: Add IoAccountingOperation enum Hanna Czenczek
                   ` (10 subsequent siblings)
  11 siblings, 1 reply; 18+ messages in thread
From: Hanna Czenczek @ 2026-10-19 18:20 UTC (permalink / raw)
  To: qemu-block
  Cc: qemu-devel, Hanna Czenczek, Kevin Wolf, John Snow,
	Denis V . Lunev, Eric Blake, Markus Armbruster, Stefan Hajnoczi,
	Paolo Bonzini

The documentation for this type is a bit sparse right now, and
specifically does not explain why it is limited to read/write only;
"I/O operation type" could well imply this should distinguish a much
broader range of operations like flushes, discards, etc., but it does
not.

Make a note why that is, which is that these types reflect the block
device on-error policy settings that we have, which are rerror for read
and werror for write.

Signed-off-by: Hanna Czenczek <hreitz@redhat.com>
---
 qapi/common.json | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/qapi/common.json b/qapi/common.json
index 1352e664b23..880eb87a9cb 100644
--- a/qapi/common.json
+++ b/qapi/common.json
@@ -9,7 +9,10 @@
 
 ##
 # @IoOperationType:
-#     An enumeration of the I/O operation types
+#     An enumeration of the I/O operation types, limited to the block
+#     device on-error policy settings we have (rerror and werror).
+#     For reference, the corresponding error policy values are set in
+#     `BlockdevOnError`.
 #
 # @read: read operation
 #
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 18+ messages in thread

* [PATCH v2 02/12] qapi/block: Add IoAccountingOperation enum
  2026-10-19 18:20 [PATCH v2 00/12] block: BLOCK_IO_DELAY event Hanna Czenczek
  2026-10-19 18:20 ` [PATCH v2 01/12] qapi/common: Clarify what IoOperationType is for Hanna Czenczek
@ 2026-10-19 18:20 ` Hanna Czenczek
  2026-10-20  5:59   ` Markus Armbruster
  2026-10-19 18:20 ` [PATCH v2 03/12] runstate: Allow IO_ERROR/SUSPENDED transitions Hanna Czenczek
                   ` (9 subsequent siblings)
  11 siblings, 1 reply; 18+ messages in thread
From: Hanna Czenczek @ 2026-10-19 18:20 UTC (permalink / raw)
  To: qemu-block
  Cc: qemu-devel, Hanna Czenczek, Kevin Wolf, John Snow,
	Denis V . Lunev, Eric Blake, Markus Armbruster, Stefan Hajnoczi,
	Paolo Bonzini

This enum gives more information than IoOperationType (which is used for
the rerror/werror distinction) to identify operation types for block
accounting.

Signed-off-by: Hanna Czenczek <hreitz@redhat.com>
---
 qapi/block.json | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

diff --git a/qapi/block.json b/qapi/block.json
index 0a88d405a26..8f4a71733c1 100644
--- a/qapi/block.json
+++ b/qapi/block.json
@@ -629,3 +629,23 @@
             'command': 'str' },
   'features': [ 'unstable' ],
   'allow-preconfig': true }
+
+##
+# @IoAccountingOperation:
+#
+# I/O operation types as they are tracked by block accounting.
+#
+# @read: Reading data
+#
+# @write: Writing data
+#
+# @flush: Flushing data to disk
+#
+# @zone-append: Appending data to a zone of a zoned block device
+#
+# @unmap: Discarding a block of data
+#
+# Since: 11.2
+##
+{ 'enum': 'IoAccountingOperation',
+  'data': [ 'read', 'write', 'flush', 'zone-append', 'unmap' ] }
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 18+ messages in thread

* [PATCH v2 03/12] runstate: Allow IO_ERROR/SUSPENDED transitions
  2026-10-19 18:20 [PATCH v2 00/12] block: BLOCK_IO_DELAY event Hanna Czenczek
  2026-10-19 18:20 ` [PATCH v2 01/12] qapi/common: Clarify what IoOperationType is for Hanna Czenczek
  2026-10-19 18:20 ` [PATCH v2 02/12] qapi/block: Add IoAccountingOperation enum Hanna Czenczek
@ 2026-10-19 18:20 ` Hanna Czenczek
  2026-10-19 18:20 ` [PATCH v2 04/12] iotests: PC-only test for I/O errors during suspend Hanna Czenczek
                   ` (8 subsequent siblings)
  11 siblings, 0 replies; 18+ messages in thread
From: Hanna Czenczek @ 2026-10-19 18:20 UTC (permalink / raw)
  To: qemu-block
  Cc: qemu-devel, Hanna Czenczek, Kevin Wolf, John Snow,
	Denis V . Lunev, Eric Blake, Markus Armbruster, Stefan Hajnoczi,
	Paolo Bonzini

I/O errors can occur while the VM is suspended, and we can continue from
a stop due to I/O error into suspension.  Allow these transitions.

Signed-off-by: Hanna Czenczek <hreitz@redhat.com>
---
 system/runstate.c | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/system/runstate.c b/system/runstate.c
index 56bfc5b07da..7b3233f77f6 100644
--- a/system/runstate.c
+++ b/system/runstate.c
@@ -106,6 +106,7 @@ static const RunStateTransition runstate_transitions_def[] = {
     { RUN_STATE_IO_ERROR, RUN_STATE_RUNNING },
     { RUN_STATE_IO_ERROR, RUN_STATE_FINISH_MIGRATE },
     { RUN_STATE_IO_ERROR, RUN_STATE_PRELAUNCH },
+    { RUN_STATE_IO_ERROR, RUN_STATE_SUSPENDED },
 
     { RUN_STATE_PAUSED, RUN_STATE_RUNNING },
     { RUN_STATE_PAUSED, RUN_STATE_FINISH_MIGRATE },
@@ -173,6 +174,7 @@ static const RunStateTransition runstate_transitions_def[] = {
     { RUN_STATE_SUSPENDED, RUN_STATE_SAVE_VM },
     { RUN_STATE_SUSPENDED, RUN_STATE_RESTORE_VM },
     { RUN_STATE_SUSPENDED, RUN_STATE_SHUTDOWN },
+    { RUN_STATE_SUSPENDED, RUN_STATE_IO_ERROR },
 
     { RUN_STATE_WATCHDOG, RUN_STATE_RUNNING },
     { RUN_STATE_WATCHDOG, RUN_STATE_FINISH_MIGRATE },
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 18+ messages in thread

* [PATCH v2 04/12] iotests: PC-only test for I/O errors during suspend
  2026-10-19 18:20 [PATCH v2 00/12] block: BLOCK_IO_DELAY event Hanna Czenczek
                   ` (2 preceding siblings ...)
  2026-10-19 18:20 ` [PATCH v2 03/12] runstate: Allow IO_ERROR/SUSPENDED transitions Hanna Czenczek
@ 2026-10-19 18:20 ` Hanna Czenczek
  2026-10-19 18:20 ` [PATCH v2 05/12] qapi/run-state: Add io-hang state Hanna Czenczek
                   ` (7 subsequent siblings)
  11 siblings, 0 replies; 18+ messages in thread
From: Hanna Czenczek @ 2026-10-19 18:20 UTC (permalink / raw)
  To: qemu-block
  Cc: qemu-devel, Hanna Czenczek, Kevin Wolf, John Snow,
	Denis V . Lunev, Eric Blake, Markus Armbruster, Stefan Hajnoczi,
	Paolo Bonzini

Using IDE (PIO) and writing directly to the PIIX4 PM device, we can test
what happens when an I/O error occurs while the VM is suspended.

The moment we resume the request while the VM is suspended is the moment
of the SUSPENDED -> IO_ERROR transition, and the moment we issue `cont`
is the reverse IO_ERROR -> SUSPENDED transition.

Signed-off-by: Hanna Czenczek <hreitz@redhat.com>
---
 .../tests/io-error-during-suspend             | 155 ++++++++++++++++++
 .../tests/io-error-during-suspend.out         |  22 +++
 2 files changed, 177 insertions(+)
 create mode 100755 tests/qemu-iotests/tests/io-error-during-suspend
 create mode 100644 tests/qemu-iotests/tests/io-error-during-suspend.out

diff --git a/tests/qemu-iotests/tests/io-error-during-suspend b/tests/qemu-iotests/tests/io-error-during-suspend
new file mode 100755
index 00000000000..5ef741d96b4
--- /dev/null
+++ b/tests/qemu-iotests/tests/io-error-during-suspend
@@ -0,0 +1,155 @@
+#!/usr/bin/env python3
+# group: rw
+#
+# Reproduce an I/O error while the guest is entering suspend
+#
+# Copyright (C) 2026 Red Hat, Inc.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+# Creator/Owner: Hanna Czenczek <hreitz@redhat.com>
+
+import iotests
+from iotests import filter_qmp_event, log
+
+iotests.script_initialize()
+
+# PC machine type required for PIIX4 and IDE
+if iotests.qemu_default_machine != 'pc':
+    iotests.notrun('Requires a PC machine')
+
+def vm_qemu_io(qvm, dev_id, cmd):
+    log(f'[HMP] qemu-io to {dev_id}: {cmd}')
+    command_line=f'qemu-io -d {dev_id} "{cmd}"'
+    log(qvm.qmp('human-monitor-command', command_line=command_line))
+
+def await_event(qvm, match):
+    evt = qvm.event_wait(match, timeout=30.0)
+    assert evt is not None
+    return evt
+
+def find_pci(qvm, vendor, device):
+    """
+    Return bus, slot, function of the device matching vendor and device ID.
+    """
+
+    for bus in qvm.qmp('query-pci')['return']:
+        for dev in bus['devices']:
+            if dev['id']['vendor'] == vendor and dev['id']['device'] == device:
+                return dev['bus'], dev['slot'], dev['function']
+
+    raise RuntimeError(f'PCI device {vendor:04x}:{device:04x} not found')
+
+def enter_s3(qvm):
+    """
+    Enter suspend via the PIIX4 PM device
+    """
+
+    bus, slot, function = find_pci(qvm, 0x8086, 0x7113)
+    pci_pm_addr = 0x80000000 | (bus << 16) | (slot << 11) | (function << 8)
+
+    # Config registers:
+    # - 0x40: Set the power management I/O range base
+    # - 0x80: Bit 0 enables that range
+    # I/O range:
+    # - +4: PM1a_CNT
+    #   - Bits 10..=12: SLEEP_TYPE (1 for suspend, see `acpi_pm_cnt_write()`)
+    #   - Bit 13: SLEEP_ENABLE
+
+    arbitrary_pm_io_base = 0xb000
+    log('Setting up PIIX4 PM I/O range')
+    # Setting the base address
+    qvm.qtest(f'outl 0xcf8 {pci_pm_addr + 0x40:#x}')
+    qvm.qtest(f'outl 0xcfc {arbitrary_pm_io_base:#x}')
+    # Enabling the range
+    qvm.qtest(f'outl 0xcf8 {pci_pm_addr + 0x80:#x}')
+    qvm.qtest('outb 0xcfc 0x1')
+
+    log('Going to sleep')
+    pm1a_cnt = arbitrary_pm_io_base + 4
+    pm1a_value = (1 << 10) | (1 << 13)
+    qvm.qtest(f'outw {pm1a_cnt:#x} {pm1a_value:#x}')
+
+def ide_issue_pio_read(qvm, lba, sectors):
+    """
+    Issue a read request from the master device.
+    Does not read the data through PIO, though.
+    """
+
+    for port, val in (
+            (0x1f6, 0xe0 | ((lba >> 24) & 0xf)), # 0x40 selects LBA
+            (0x1f2, sectors),
+            (0x1f3, lba & 0xff),
+            (0x1f4, (lba >> 8) & 0xff),
+            (0x1f5, (lba >> 16) & 0xff),
+            (0x1f7, 0x20), # WIN_READ
+        ):
+        qvm.qtest(f'outb {port:#x} {val:#x}')
+
+with iotests.VM() as vm:
+    # So we have PIIX4
+    vm.add_args('-machine', 'pc')
+
+    failing_sector = 42
+    vm.add_blockdev(vm.qmp_to_opts({
+        'driver': 'raw',
+        'node-name': 'test',
+        'file': {
+            'driver': 'blkdebug',
+            'inject-error': [{'event': 'read_aio', 'iotype': 'read',
+                              'sector': str(failing_sector), 'once': 'on'}],
+            'image': {
+                'driver': 'null-co',
+            },
+        },
+    }))
+
+    disk = 'ide-disk'
+    vm.add_device(f'ide-hd,id={disk},drive=test,bus=ide.0,unit=0,'
+                   'rerror=stop,werror=stop')
+
+    vm.launch()
+
+    try:
+        # Issue a read request from a guest device (to the failing sector), and
+        # have it pause before it returns
+        vm_qemu_io(vm, disk, 'break read_aio r0')
+        log('Issuing read')
+        ide_issue_pio_read(vm, failing_sector, 1)
+        vm_qemu_io(vm, disk, 'wait_break r0')
+
+        # Read request is paused now, let's suspend the VM
+        log('Entering suspend')
+        enter_s3(vm)
+        log(await_event(vm, 'SUSPEND'), filters=[filter_qmp_event])
+
+        # Should be suspended now
+        log('[QMP] query-status')
+        log(vm.qmp('query-status'))
+    except Exception:
+        # The VM will not be able to quit without this request finished, which
+        # (without this block) would make the test hang instead of exit if
+        # something unexpected goes wrong
+        vm_qemu_io(vm, disk, 'resume r0')
+        raise
+
+    # Resume the request, which should run into an error thanks to our blkdebug
+    # configuration
+    vm_qemu_io(vm, disk, 'resume r0')
+    log(await_event(vm, 'BLOCK_IO_ERROR'), filters=[filter_qmp_event])
+    log(await_event(vm, 'STOP'), filters=[filter_qmp_event])
+
+    # Should be stopped now (not just suspended)
+    log('[QMP] query-status')
+    log(vm.qmp('query-status'))
+
+    log('[QMP] cont')
+    log(vm.qmp('cont'))
+
+    log(await_event(vm, 'RESUME'), filters=[filter_qmp_event])
+
+    # Should be suspended again (error was injected 'once', so should be gone)
+    log('[QMP] query-status')
+    log(vm.qmp('query-status'))
+
+    vm.shutdown()
diff --git a/tests/qemu-iotests/tests/io-error-during-suspend.out b/tests/qemu-iotests/tests/io-error-during-suspend.out
new file mode 100644
index 00000000000..353f18250ad
--- /dev/null
+++ b/tests/qemu-iotests/tests/io-error-during-suspend.out
@@ -0,0 +1,22 @@
+[HMP] qemu-io to ide-disk: break read_aio r0
+{"return": ""}
+Issuing read
+[HMP] qemu-io to ide-disk: wait_break r0
+{"return": ""}
+Entering suspend
+Setting up PIIX4 PM I/O range
+Going to sleep
+{"event": "SUSPEND", "timestamp": {"microseconds": "USECS", "seconds": "SECS"}}
+[QMP] query-status
+{"return": {"running": false, "status": "suspended"}}
+[HMP] qemu-io to ide-disk: resume r0
+{"return": ""}
+{"data": {"action": "stop", "device": "", "node-name": "test", "nospace": false, "operation": "read", "qom-path": "/machine/peripheral/ide-disk", "reason": "Input/output error"}, "event": "BLOCK_IO_ERROR", "timestamp": {"microseconds": "USECS", "seconds": "SECS"}}
+{"event": "STOP", "timestamp": {"microseconds": "USECS", "seconds": "SECS"}}
+[QMP] query-status
+{"return": {"running": false, "status": "io-error"}}
+[QMP] cont
+{"return": {}}
+{"event": "RESUME", "timestamp": {"microseconds": "USECS", "seconds": "SECS"}}
+[QMP] query-status
+{"return": {"running": false, "status": "suspended"}}
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 18+ messages in thread

* [PATCH v2 05/12] qapi/run-state: Add io-hang state
  2026-10-19 18:20 [PATCH v2 00/12] block: BLOCK_IO_DELAY event Hanna Czenczek
                   ` (3 preceding siblings ...)
  2026-10-19 18:20 ` [PATCH v2 04/12] iotests: PC-only test for I/O errors during suspend Hanna Czenczek
@ 2026-10-19 18:20 ` Hanna Czenczek
  2026-10-20  6:02   ` Markus Armbruster
  2026-10-19 18:20 ` [PATCH v2 06/12] qapi/block: Add BLOCK_IO_DELAY event Hanna Czenczek
                   ` (6 subsequent siblings)
  11 siblings, 1 reply; 18+ messages in thread
From: Hanna Czenczek @ 2026-10-19 18:20 UTC (permalink / raw)
  To: qemu-block
  Cc: qemu-devel, Hanna Czenczek, Kevin Wolf, John Snow,
	Denis V . Lunev, Eric Blake, Markus Armbruster, Stefan Hajnoczi,
	Paolo Bonzini

This state is similar to io-error, but is not entered on I/O errors but
instead when a request takes longer than a user-defined time (set via
the delay-stop-ns property on a block device, which is not yet available
but will be added by a future patch in this series).

In contrast to other stop states, this state does not drain block
devices.  That is because it is entered only when a request hangs, which
means that draining would be counter-productive.

Signed-off-by: Hanna Czenczek <hreitz@redhat.com>
---
 qapi/run-state.json |  7 ++++++-
 system/runstate.c   | 25 ++++++++++++++++++-------
 2 files changed, 24 insertions(+), 8 deletions(-)

diff --git a/qapi/run-state.json b/qapi/run-state.json
index 8bbd9884087..cf3f43579de 100644
--- a/qapi/run-state.json
+++ b/qapi/run-state.json
@@ -53,12 +53,17 @@
 # @colo: guest is paused to save/restore VM state under colo
 #     checkpoint, VM can not get into this state unless colo
 #     capability is enabled for migration.  (since 2.8)
+#
+# @io-hang: An I/O operation has been delayed for too long and the
+#     device is configured to stop the VM when that happens.  In
+#     contrast to other paused states, the VM's block devices will not
+#     be drained when entering this run state.  (since 11.2)
 ##
 { 'enum': 'RunState',
   'data': [ 'debug', 'inmigrate', 'internal-error', 'io-error', 'paused',
             'postmigrate', 'prelaunch', 'finish-migrate', 'restore-vm',
             'running', 'save-vm', 'shutdown', 'suspended', 'watchdog',
-            'guest-panicked', 'colo' ] }
+            'guest-panicked', 'colo', 'io-hang' ] }
 
 ##
 # @ShutdownCause:
diff --git a/system/runstate.c b/system/runstate.c
index 7b3233f77f6..c5b56308735 100644
--- a/system/runstate.c
+++ b/system/runstate.c
@@ -98,6 +98,7 @@ static const RunStateTransition runstate_transitions_def[] = {
     { RUN_STATE_INMIGRATE, RUN_STATE_PRELAUNCH },
     { RUN_STATE_INMIGRATE, RUN_STATE_POSTMIGRATE },
     { RUN_STATE_INMIGRATE, RUN_STATE_COLO },
+    { RUN_STATE_INMIGRATE, RUN_STATE_IO_HANG },
 
     { RUN_STATE_INTERNAL_ERROR, RUN_STATE_PAUSED },
     { RUN_STATE_INTERNAL_ERROR, RUN_STATE_FINISH_MIGRATE },
@@ -135,6 +136,7 @@ static const RunStateTransition runstate_transitions_def[] = {
     { RUN_STATE_FINISH_MIGRATE, RUN_STATE_SUSPENDED },
     { RUN_STATE_FINISH_MIGRATE, RUN_STATE_WATCHDOG },
     { RUN_STATE_FINISH_MIGRATE, RUN_STATE_GUEST_PANICKED },
+    { RUN_STATE_FINISH_MIGRATE, RUN_STATE_IO_HANG },
 
     { RUN_STATE_RESTORE_VM, RUN_STATE_RUNNING },
     { RUN_STATE_RESTORE_VM, RUN_STATE_PRELAUNCH },
@@ -155,6 +157,7 @@ static const RunStateTransition runstate_transitions_def[] = {
     { RUN_STATE_RUNNING, RUN_STATE_WATCHDOG },
     { RUN_STATE_RUNNING, RUN_STATE_GUEST_PANICKED },
     { RUN_STATE_RUNNING, RUN_STATE_COLO},
+    { RUN_STATE_RUNNING, RUN_STATE_IO_HANG },
 
     { RUN_STATE_SAVE_VM, RUN_STATE_RUNNING },
     { RUN_STATE_SAVE_VM, RUN_STATE_SUSPENDED },
@@ -175,6 +178,7 @@ static const RunStateTransition runstate_transitions_def[] = {
     { RUN_STATE_SUSPENDED, RUN_STATE_RESTORE_VM },
     { RUN_STATE_SUSPENDED, RUN_STATE_SHUTDOWN },
     { RUN_STATE_SUSPENDED, RUN_STATE_IO_ERROR },
+    { RUN_STATE_SUSPENDED, RUN_STATE_IO_HANG },
 
     { RUN_STATE_WATCHDOG, RUN_STATE_RUNNING },
     { RUN_STATE_WATCHDOG, RUN_STATE_FINISH_MIGRATE },
@@ -185,6 +189,11 @@ static const RunStateTransition runstate_transitions_def[] = {
     { RUN_STATE_GUEST_PANICKED, RUN_STATE_FINISH_MIGRATE },
     { RUN_STATE_GUEST_PANICKED, RUN_STATE_PRELAUNCH },
 
+    { RUN_STATE_IO_HANG, RUN_STATE_RUNNING },
+    { RUN_STATE_IO_HANG, RUN_STATE_FINISH_MIGRATE },
+    { RUN_STATE_IO_HANG, RUN_STATE_PRELAUNCH },
+    { RUN_STATE_IO_HANG, RUN_STATE_SUSPENDED },
+
     { RUN_STATE__MAX, RUN_STATE__MAX },
 };
 
@@ -445,13 +454,15 @@ static int do_vm_stop(RunState state, bool send_stop)
         }
     }
 
-    bdrv_drain_all();
-    /*
-     * Even if vm_state_notify() return failure,
-     * it would be better to flush as before.
-     */
-    ret |= bdrv_flush_all();
-    trace_vm_stop_flush_all(ret);
+    if (state != RUN_STATE_IO_HANG) {
+        bdrv_drain_all();
+        /*
+         * Even if vm_state_notify() return failure,
+         * it would be better to flush as before.
+         */
+        ret |= bdrv_flush_all();
+        trace_vm_stop_flush_all(ret);
+    }
 
     return ret;
 }
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 18+ messages in thread

* [PATCH v2 06/12] qapi/block: Add BLOCK_IO_DELAY event
  2026-10-19 18:20 [PATCH v2 00/12] block: BLOCK_IO_DELAY event Hanna Czenczek
                   ` (4 preceding siblings ...)
  2026-10-19 18:20 ` [PATCH v2 05/12] qapi/run-state: Add io-hang state Hanna Czenczek
@ 2026-10-19 18:20 ` Hanna Czenczek
  2026-10-20  6:49   ` Markus Armbruster
  2026-10-19 18:20 ` [PATCH v2 07/12] qapi/block: Add BLOCK_IO_DONE event Hanna Czenczek
                   ` (5 subsequent siblings)
  11 siblings, 1 reply; 18+ messages in thread
From: Hanna Czenczek @ 2026-10-19 18:20 UTC (permalink / raw)
  To: qemu-block
  Cc: qemu-devel, Hanna Czenczek, Kevin Wolf, John Snow,
	Denis V . Lunev, Eric Blake, Markus Armbruster, Stefan Hajnoczi,
	Paolo Bonzini

This event will be emitted when a disk operation takes longer than a
pre-defined threshold (as set on the block device).

Note that the description refers to the "delay-report-ns" and
"delay-stop-ns" block device properties, which are not yet available.
A future commit in this series will add them.

Signed-off-by: Hanna Czenczek <hreitz@redhat.com>
---
 qapi/block.json | 37 +++++++++++++++++++++++++++++++++++++
 1 file changed, 37 insertions(+)

diff --git a/qapi/block.json b/qapi/block.json
index 8f4a71733c1..5f9f5c28050 100644
--- a/qapi/block.json
+++ b/qapi/block.json
@@ -649,3 +649,40 @@
 ##
 { 'enum': 'IoAccountingOperation',
   'data': [ 'read', 'write', 'flush', 'zone-append', 'unmap' ] }
+
+##
+# @BLOCK_IO_DELAY:
+#
+# Emitted when a disk operation takes longer than the pre-defined
+# threshold (as set via the "delay-report-ns" / "delay-stop-ns"
+# properties on the block device).
+#
+# Note that delays deliberately incurred by throttling do count
+# towards the operation duration.
+#
+# @qom-path: path to the block device object in the QOM tree
+#
+# @operation: I/O operation
+#
+# @latency-ns: Ongoing duration of the operation (in nanoseconds)
+#
+# @action: Action that has been taken ("report" for "delay-report-ns"
+#     or "stop" for "delay-stop-ns").  A "stop" action means the VM
+#     will be stopped with the "io-hang" `RunState`, which does not
+#     drain the block devices attached to the VM, because draining
+#     would be counterproductive in case of a hanging I/O request.
+#
+# @offset: Request offset in the guest disk (in bytes), if available
+#
+# @bytes: Request length
+#
+# Since: 11.2
+##
+{ 'event': 'BLOCK_IO_DELAY',
+  'data': {
+      'qom-path': 'str',
+      'operation': 'IoAccountingOperation',
+      'latency-ns': 'uint64',
+      'action': 'BlockErrorAction',
+      '*offset': 'uint64',
+      'bytes': 'uint64' } }
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 18+ messages in thread

* [PATCH v2 07/12] qapi/block: Add BLOCK_IO_DONE event
  2026-10-19 18:20 [PATCH v2 00/12] block: BLOCK_IO_DELAY event Hanna Czenczek
                   ` (5 preceding siblings ...)
  2026-10-19 18:20 ` [PATCH v2 06/12] qapi/block: Add BLOCK_IO_DELAY event Hanna Czenczek
@ 2026-10-19 18:20 ` Hanna Czenczek
  2026-10-20  6:55   ` Markus Armbruster
  2026-10-19 18:20 ` [PATCH v2 08/12] block-backend: Public blk_get_attached_dev_path() Hanna Czenczek
                   ` (4 subsequent siblings)
  11 siblings, 1 reply; 18+ messages in thread
From: Hanna Czenczek @ 2026-10-19 18:20 UTC (permalink / raw)
  To: qemu-block
  Cc: qemu-devel, Hanna Czenczek, Kevin Wolf, John Snow,
	Denis V . Lunev, Eric Blake, Markus Armbruster, Stefan Hajnoczi,
	Paolo Bonzini

This event is emitted when an operation completes, for which a
BLOCK_IO_DELAY event has previously been emitted.

Signed-off-by: Hanna Czenczek <hreitz@redhat.com>
---
 qapi/block.json | 33 +++++++++++++++++++++++++++++++++
 1 file changed, 33 insertions(+)

diff --git a/qapi/block.json b/qapi/block.json
index 5f9f5c28050..38f293dd32d 100644
--- a/qapi/block.json
+++ b/qapi/block.json
@@ -660,6 +660,9 @@
 # Note that delays deliberately incurred by throttling do count
 # towards the operation duration.
 #
+# This event will only be emitted once per request and action.  When
+# the operation does finish, a `BLOCK_IO_DONE` event is emitted.
+#
 # @qom-path: path to the block device object in the QOM tree
 #
 # @operation: I/O operation
@@ -686,3 +689,33 @@
       'action': 'BlockErrorAction',
       '*offset': 'uint64',
       'bytes': 'uint64' } }
+
+##
+# @BLOCK_IO_DONE:
+#
+# Emitted on completion for disk operations that previously emitted a
+# `BLOCK_IO_DELAY` event.
+#
+# @qom-path: path to the block device object in the QOM tree
+#
+# @operation: I/O operation
+#
+# @latency-ns: Full duration of the operation (in nanoseconds)
+#
+# @offset: Request offset in the guest disk (in bytes), if available
+#
+# @bytes: Request length
+#
+# @failed: If true, this request failed for any reason.  If false, the
+#     request succeeded.
+#
+# Since: 11.2
+##
+{ 'event': 'BLOCK_IO_DONE',
+  'data': {
+      'qom-path': 'str',
+      'operation': 'IoAccountingOperation',
+      'latency-ns': 'uint64',
+      '*offset': 'uint64',
+      'bytes': 'uint64',
+      'failed': 'bool' } }
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 18+ messages in thread

* [PATCH v2 08/12] block-backend: Public blk_get_attached_dev_path()
  2026-10-19 18:20 [PATCH v2 00/12] block: BLOCK_IO_DELAY event Hanna Czenczek
                   ` (6 preceding siblings ...)
  2026-10-19 18:20 ` [PATCH v2 07/12] qapi/block: Add BLOCK_IO_DONE event Hanna Czenczek
@ 2026-10-19 18:20 ` Hanna Czenczek
  2026-10-19 18:20 ` [PATCH v2 09/12] host-utils: Add u{add,sub}{32,64}_saturate() Hanna Czenczek
                   ` (3 subsequent siblings)
  11 siblings, 0 replies; 18+ messages in thread
From: Hanna Czenczek @ 2026-10-19 18:20 UTC (permalink / raw)
  To: qemu-block
  Cc: qemu-devel, Hanna Czenczek, Kevin Wolf, John Snow,
	Denis V . Lunev, Eric Blake, Markus Armbruster, Stefan Hajnoczi,
	Paolo Bonzini

Make blk_get_attached_dev_path() public so we can use it in the I/O
operation delay QMP event to identify the affected guest block device.

Signed-off-by: Hanna Czenczek <hreitz@redhat.com>
---
 include/system/block-backend-io.h | 9 +++++++++
 block/block-backend.c             | 6 +-----
 2 files changed, 10 insertions(+), 5 deletions(-)

diff --git a/include/system/block-backend-io.h b/include/system/block-backend-io.h
index fd84723d9d0..63b2dd2e24f 100644
--- a/include/system/block-backend-io.h
+++ b/include/system/block-backend-io.h
@@ -41,6 +41,15 @@ bool blk_iostatus_is_enabled(const BlockBackend *blk);
  */
 char *blk_get_attached_dev_id(BlockBackend *blk);
 
+/*
+ * Return the QOM path of the block device attached to the
+ * BlockBackend.
+ *
+ * The caller is responsible for releasing the value returned
+ * with g_free() after use.
+ */
+char *blk_get_attached_dev_path(BlockBackend *blk);
+
 BlockAIOCB *blk_aio_pwrite_zeroes(BlockBackend *blk, int64_t offset,
                                   int64_t bytes, BdrvRequestFlags flags,
                                   BlockCompletionFunc *cb, void *opaque);
diff --git a/block/block-backend.c b/block/block-backend.c
index ab837fac921..78678986466 100644
--- a/block/block-backend.c
+++ b/block/block-backend.c
@@ -1054,11 +1054,7 @@ char *blk_get_attached_dev_id(BlockBackend *blk)
     return blk_get_attached_dev_id_or_path(blk, true);
 }
 
-/*
- * The caller is responsible for releasing the value returned
- * with g_free() after use.
- */
-static char *blk_get_attached_dev_path(BlockBackend *blk)
+char *blk_get_attached_dev_path(BlockBackend *blk)
 {
     return blk_get_attached_dev_id_or_path(blk, false);
 }
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 18+ messages in thread

* [PATCH v2 09/12] host-utils: Add u{add,sub}{32,64}_saturate()
  2026-10-19 18:20 [PATCH v2 00/12] block: BLOCK_IO_DELAY event Hanna Czenczek
                   ` (7 preceding siblings ...)
  2026-10-19 18:20 ` [PATCH v2 08/12] block-backend: Public blk_get_attached_dev_path() Hanna Czenczek
@ 2026-10-19 18:20 ` Hanna Czenczek
  2026-10-19 18:20 ` [PATCH v2 10/12] block: Per-request delay tracker Hanna Czenczek
                   ` (2 subsequent siblings)
  11 siblings, 0 replies; 18+ messages in thread
From: Hanna Czenczek @ 2026-10-19 18:20 UTC (permalink / raw)
  To: qemu-block
  Cc: qemu-devel, Hanna Czenczek, Kevin Wolf, John Snow,
	Denis V . Lunev, Eric Blake, Markus Armbruster, Stefan Hajnoczi,
	Paolo Bonzini

Add saturating addition/subtraction functions for unsigned 32 and 64 bit
values.

Signed-off-by: Hanna Czenczek <hreitz@redhat.com>
---
 include/qemu/host-utils.h | 62 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 62 insertions(+)

diff --git a/include/qemu/host-utils.h b/include/qemu/host-utils.h
index 1db9dbb138e..848e04c8fb1 100644
--- a/include/qemu/host-utils.h
+++ b/include/qemu/host-utils.h
@@ -636,6 +636,36 @@ static inline int64_t sadd64_saturate(int64_t x, int64_t y)
     return ret;
 }
 
+/**
+ * uadd32_saturate - 32-bit unsigned addition with saturation
+ * @x, @y: addends
+ *
+ * Computes @x + @y, and saturates rather than truncating the result.
+ */
+static inline uint32_t uadd32_saturate(uint32_t x, uint32_t y)
+{
+    uint32_t ret;
+    if (uadd32_overflow(x, y, &ret)) {
+        ret = UINT32_MAX;
+    }
+    return ret;
+}
+
+/**
+ * uadd64_saturate - 64-bit unsigned addition with saturation
+ * @x, @y: addends
+ *
+ * Computes @x + @y, and saturates rather than truncating the result.
+ */
+static inline uint64_t uadd64_saturate(uint64_t x, uint64_t y)
+{
+    uint64_t ret;
+    if (uadd64_overflow(x, y, &ret)) {
+        ret = UINT64_MAX;
+    }
+    return ret;
+}
+
 /**
  * ssub32_saturate - 32-bit signed subtraction with saturation
  * @x, @y: addends
@@ -666,6 +696,38 @@ static inline int64_t ssub64_saturate(int64_t x, int64_t y)
     return ret;
 }
 
+/**
+ * usub32_saturate - 32-bit unsigned subtraction with saturation
+ * @x: Minuend
+ * @y: Subtrahend
+ *
+ * Computes @x - @y, and saturates rather than truncating the result.
+ */
+static inline uint32_t usub32_saturate(uint32_t x, uint32_t y)
+{
+    uint32_t ret;
+    if (usub32_overflow(x, y, &ret)) {
+        ret = 0;
+    }
+    return ret;
+}
+
+/**
+ * usub64_saturate - 64-bit unsigned subtraction with saturation
+ * @x: Minuend
+ * @y: Subtrahend
+ *
+ * Computes @x - @y, and saturates rather than truncating the result.
+ */
+static inline uint64_t usub64_saturate(uint64_t x, uint64_t y)
+{
+    uint64_t ret;
+    if (usub64_overflow(x, y, &ret)) {
+        ret = 0;
+    }
+    return ret;
+}
+
 /*
  * Unsigned 128x64 multiplication.
  * Returns true if the result got truncated to 128 bits.
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 18+ messages in thread

* [PATCH v2 10/12] block: Per-request delay tracker
  2026-10-19 18:20 [PATCH v2 00/12] block: BLOCK_IO_DELAY event Hanna Czenczek
                   ` (8 preceding siblings ...)
  2026-10-19 18:20 ` [PATCH v2 09/12] host-utils: Add u{add,sub}{32,64}_saturate() Hanna Czenczek
@ 2026-10-19 18:20 ` Hanna Czenczek
  2026-10-19 18:20 ` [PATCH v2 11/12] block: Add delay-report-ns and delay-stop-ns props Hanna Czenczek
  2026-10-19 18:20 ` [PATCH v2 12/12] iotests: Add io-delay-reporting test Hanna Czenczek
  11 siblings, 0 replies; 18+ messages in thread
From: Hanna Czenczek @ 2026-10-19 18:20 UTC (permalink / raw)
  To: qemu-block
  Cc: qemu-devel, Hanna Czenczek, Kevin Wolf, John Snow,
	Denis V . Lunev, Eric Blake, Markus Armbruster, Stefan Hajnoczi,
	Paolo Bonzini

Register I/O requests to BlockBackends with a per-BB timer running in
the background (in the main context) that goes through the list of
requests to find any that surpass a user-defined[1] threshold and then
emit a QMP event calling this out, or stopping the VM altogether so the
guest can be shielded of absolutely excessive delay.

One example for where stopping may be useful: Windows will cancel
requests after a set timeout, but virtio-blk does not support cancelling
requests (by design, I understand).  So it will cancel again, and again,
and then reset the whole driver (which however won’t cancel the request
either), and eventually declare the driver faulty and show a BSOD.  The
only ways around this would be to implement request cancellation (which
I think is not generally possible), or, well, pause the VM in case of
absolutely excessive I/O request delay, and resume it when the request
is done (BLOCK_IO_DONE event), so that Windows will not see that massive
delay.

[1] Not yet user-defined in this commit, but a future commit will allow
    users to specify the threshold when to take action, on the guest
    device.

Signed-off-by: Hanna Czenczek <hreitz@redhat.com>
---
 include/block/block-backend-delay.h         | 112 +++++++
 include/system/block-backend-global-state.h |  11 +
 block/block-backend-delay.c                 | 326 ++++++++++++++++++++
 block/block-backend.c                       |  44 ++-
 block/meson.build                           |   1 +
 5 files changed, 492 insertions(+), 2 deletions(-)
 create mode 100644 include/block/block-backend-delay.h
 create mode 100644 block/block-backend-delay.c

diff --git a/include/block/block-backend-delay.h b/include/block/block-backend-delay.h
new file mode 100644
index 00000000000..9af2e6de925
--- /dev/null
+++ b/include/block/block-backend-delay.h
@@ -0,0 +1,112 @@
+/*
+ * Copyright (c) 2026 Hanna Czenczek <hreitz@redhat.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#ifndef BLOCK_BACKEND_DELAY_H
+#define BLOCK_BACKEND_DELAY_H
+
+#include "qemu/thread.h"
+#include "qemu/timer.h"
+#include "qapi/qapi-types-block.h"
+
+/* Enumeration of I/O delay threshold types */
+enum BlockDelayThreshold {
+    /* Latency threshold at which to report a delay */
+    BLOCK_DELAY_REPORT,
+    /* Latency threshold at which to auto-stop the VM */
+    BLOCK_DELAY_STOP,
+
+    /* Number of different thresholds */
+    BLOCK_DELAY__MAX,
+};
+
+typedef struct BlockDelayTracker {
+    /* Timer to regularly check requests for delay */
+    QEMUTimer *timer;
+
+    /* Which clock to use */
+    QEMUClockType clock_type;
+
+    /*
+     * Protects access to all subsequent fields in this struct. Do not take any
+     * other lock while this lock is taken.
+     */
+    QemuMutex lock;
+
+    /* Thresholds for generating delay report events or stopping the VM */
+    uint64_t threshold_ns[BLOCK_DELAY__MAX];
+
+    /* List of active requests; new requests appended at the end */
+    QTAILQ_HEAD(, BlockDelayTrackerRequest) requests;
+} BlockDelayTracker;
+
+typedef struct BlockDelayTrackerRequest {
+    /* offset < 0 means no valid offset */
+    int64_t offset;
+    /* Request length, or 0 if no valid length */
+    uint64_t bytes;
+    /* When the request was started */
+    uint64_t start_time_ns;
+    /* Operation type */
+    IoAccountingOperation op_type;
+    /*
+     * Whether any delay threshold has been exceeded.
+     * If so, we need to emit BLOCK_IO_DONE when the request is done.
+     */
+    bool threshold_exceeded[BLOCK_DELAY__MAX];
+    /*
+     * Set the first time any threshold is exceeded, to cache the device name
+     * for BLOCK_IO_DONE.
+     */
+    char *device_path;
+
+    /* Entry in the list of all running requests for this BB */
+    QTAILQ_ENTRY(BlockDelayTrackerRequest) list;
+} BlockDelayTrackerRequest;
+
+/**
+ * Set up the request latency delay tracker for this BlockBackend.
+ *
+ * The caller must allocate blk->delay_tracker beforehand (because
+ * block-backend-delay.c cannot access BlockBackend fields directly).
+ */
+void blk_delay_tracker_init(BlockBackend *blk);
+
+/**
+ * Stop and destroy the request latency delay tracker for this BlockBackend.
+ */
+void blk_delay_tracker_destroy(BlockDelayTracker *bdt);
+
+/**
+ * Return the latency delay tracker object for the given BlockBackend.
+ *
+ * (Implemented in block-backend.c, where the BlockBackend definition resides.)
+ */
+BlockDelayTracker *blk_delay_tracker(BlockBackend *blk);
+
+/**
+ * Begin tracking an I/O request on @blk of type @op_type over @bytes starting
+ * from @offset.
+ *
+ * @request is initialized by this function.
+ *
+ * Use offset=-1 for operations that have no valid offset (e.g. flush).
+ */
+void blk_track_delay_begin(BlockBackend *blk,
+                           BlockDelayTrackerRequest *request,
+                           int64_t offset,
+                           uint64_t bytes,
+                           IoAccountingOperation op_type);
+/**
+ * Finalize I/O request tracking.
+ *
+ * @ret indicates the operation's status, with 0 for success and -errno for
+ * errors.
+ */
+void blk_track_delay_end(BlockBackend *blk,
+                         BlockDelayTrackerRequest *request,
+                         int ret);
+
+#endif
diff --git a/include/system/block-backend-global-state.h b/include/system/block-backend-global-state.h
index c3849640df9..c04fcff81a7 100644
--- a/include/system/block-backend-global-state.h
+++ b/include/system/block-backend-global-state.h
@@ -102,6 +102,17 @@ void blk_update_root_state(BlockBackend *blk);
 bool blk_get_detect_zeroes_from_root_state(BlockBackend *blk);
 int blk_get_open_flags_from_root_state(BlockBackend *blk);
 
+/**
+ * Set the nanosecond threshold for per-request latency, when to consider it
+ * sufficient of a delay to report via BLOCK_IO_DELAY QMP event.
+ */
+void blk_set_delay_report_threshold(BlockBackend *blk, uint64_t ns);
+/**
+ * Set the nanosecond threshold for per-request latency, when to consider it
+ * sufficient of a delay to stop the VM.
+ */
+void blk_set_delay_stop_threshold(BlockBackend *blk, uint64_t ns);
+
 int blk_save_vmstate(BlockBackend *blk, const uint8_t *buf,
                      int64_t pos, int size);
 int blk_load_vmstate(BlockBackend *blk, uint8_t *buf, int64_t pos, int size);
diff --git a/block/block-backend-delay.c b/block/block-backend-delay.c
new file mode 100644
index 00000000000..07891573861
--- /dev/null
+++ b/block/block-backend-delay.c
@@ -0,0 +1,326 @@
+/*
+ * Keep track of ongoing BlockBackend requests, notify about excessive
+ * latency (i.e. delays), and potentially even pause the VM until such a
+ * request is done.
+ *
+ * Copyright (c) 2026 Hanna Czenczek <hreitz@redhat.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "qemu/osdep.h"
+#include "block/block-backend-delay.h"
+#include "qapi/qapi-events-block.h"
+#include "system/block-backend.h"
+#include "system/qtest.h"
+#include "system/runstate.h"
+
+/**
+ * Information for BLOCK_IO_DELAY/DONE events so that:
+ * - delay_track_timer() can collect all of the events to emit in a list and
+ *   emit them outside of the BlockDelayTracker lock.
+ * - blk_track_delay_end() can move the event emission to the main loop for
+ *   ordering
+ */
+typedef struct DelayEvent DelayEvent;
+struct DelayEvent {
+    char *device_path;
+    IoAccountingOperation op_type;
+    uint64_t duration_ns;
+    union {
+        BlockErrorAction action; /* for BLOCK_IO_DELAY */
+        int ret;                 /* for BLOCK_IO_DONE */
+    };
+    bool has_offset;
+    uint64_t offset;
+    uint64_t bytes;
+    QTAILQ_ENTRY(DelayEvent) next;
+};
+
+static void delay_track_timer(void *opaque);
+static void send_done_event(void *opaque);
+
+void blk_delay_tracker_init(BlockBackend *blk)
+{
+    BlockDelayTracker *bdt = blk_delay_tracker(blk);
+
+    assert(bdt != NULL);
+
+    bdt->clock_type = qtest_enabled() ? QEMU_CLOCK_VIRTUAL
+                                      : QEMU_CLOCK_REALTIME;
+
+    qemu_mutex_init(&bdt->lock);
+    QTAILQ_INIT(&bdt->requests);
+
+    bdt->timer = aio_timer_new(qemu_get_aio_context(), bdt->clock_type,
+                               SCALE_NS, delay_track_timer, blk);
+}
+
+void blk_delay_tracker_destroy(BlockDelayTracker *bdt)
+{
+    timer_free(bdt->timer);
+    qemu_mutex_destroy(&bdt->lock);
+    assert(QTAILQ_EMPTY(&bdt->requests));
+    g_free(bdt);
+}
+
+void blk_set_delay_report_threshold(BlockBackend *blk, uint64_t ns)
+{
+    BlockDelayTracker *bdt = blk_delay_tracker(blk);
+
+    WITH_QEMU_LOCK_GUARD(&bdt->lock) {
+        bdt->threshold_ns[BLOCK_DELAY_REPORT] = ns;
+    }
+
+    /* Wake the timer immediately to adjust to the new threshold */
+    timer_mod(bdt->timer, 0);
+}
+
+void blk_set_delay_stop_threshold(BlockBackend *blk, uint64_t ns)
+{
+    BlockDelayTracker *bdt = blk_delay_tracker(blk);
+
+    WITH_QEMU_LOCK_GUARD(&bdt->lock) {
+        bdt->threshold_ns[BLOCK_DELAY_STOP] = ns;
+    }
+
+    /* Wake the timer immediately to adjust to the new threshold */
+    timer_mod(bdt->timer, 0);
+}
+
+void blk_track_delay_begin(BlockBackend *blk,
+                           BlockDelayTrackerRequest *request,
+                           int64_t offset,
+                           uint64_t bytes,
+                           IoAccountingOperation op_type)
+{
+    BlockDelayTracker *bdt = blk_delay_tracker(blk);
+
+    *request = (BlockDelayTrackerRequest) {
+        .offset = offset,
+        .bytes = bytes,
+        .op_type = op_type,
+    };
+
+    WITH_QEMU_LOCK_GUARD(&bdt->lock) {
+        if (!bdt->threshold_ns[BLOCK_DELAY_REPORT] &&
+            !bdt->threshold_ns[BLOCK_DELAY_STOP]) {
+            /* Must be so by 0-initialization of .list */
+            assert(!QTAILQ_IN_USE(request, list));
+            return;
+        }
+
+        /* Set this under the lock so `bdt->requests` is correctly ordered */
+        request->start_time_ns = qemu_clock_get_ns(bdt->clock_type);
+        QTAILQ_INSERT_TAIL(&bdt->requests, request, list);
+    }
+
+}
+
+void blk_track_delay_end(BlockBackend *blk,
+                         BlockDelayTrackerRequest *request,
+                         int ret)
+{
+    BlockDelayTracker *bdt = blk_delay_tracker(blk);
+    bool need_done_event = false;
+
+    if (!QTAILQ_IN_USE(request, list)) {
+        return;
+    }
+
+    WITH_QEMU_LOCK_GUARD(&bdt->lock) {
+        for (int i = 0; i < BLOCK_DELAY__MAX; i++) {
+            need_done_event |= request->threshold_exceeded[i];
+        }
+        QTAILQ_REMOVE(&bdt->requests, request, list);
+    }
+
+    /* `device_path` is set if and only if any `threshold_exceeded` are set */
+    assert(!!request->device_path == need_done_event);
+
+    if (need_done_event) {
+        DelayEvent *event = g_new(DelayEvent, 1);
+        uint64_t now = qemu_clock_get_ns(bdt->clock_type);
+
+        *event = (DelayEvent) {
+            .device_path = request->device_path,
+            .op_type = request->op_type,
+            .duration_ns = usub64_saturate(now, request->start_time_ns),
+            .ret = ret,
+            .has_offset = request->offset >= 0,
+            .offset = request->offset,
+            .bytes = request->bytes,
+        };
+
+        /*
+         * Emit the event in the main loop, where the timer runs, too, so we
+         * always emit BLOCK_IO_DONE after all corresponding BLOCK_IO_DELAY
+         * events.
+         */
+        aio_bh_schedule_oneshot(qemu_get_aio_context(), send_done_event, event);
+    }
+}
+
+/**
+ * Send a BLOCK_IO_DONE event from the `DelayEvent` given via `opaque`.
+ *
+ * This event, and the `device_path` in it, are freed via `g_free()`.
+ */
+static void send_done_event(void *opaque)
+{
+    DelayEvent *event = opaque;
+
+    qapi_event_send_block_io_done(event->device_path,
+                                  event->op_type,
+                                  event->duration_ns,
+                                  event->has_offset, event->offset,
+                                  event->bytes,
+                                  event->ret < 0);
+    g_free(event->device_path);
+    g_free(event);
+}
+
+/**
+ * Track and periodically check for stalled requests.
+ *
+ * @opaque is the `BlockBackend`.
+ *
+ * Emits events when requests take longer than the BLOCK_DELAY_REPORT
+ * threshold, and stops the VM if one takes longer than the BLOCK_DELAY_STOP
+ * threshold.
+ *
+ * Handles spurious wakes.
+ */
+static void delay_track_timer(void *opaque)
+{
+    BlockBackend *blk = opaque;
+    BlockDelayTracker *bdt = blk_delay_tracker(blk);
+    g_autofree char *dev_path = NULL;
+    bool stop_vm = false;
+    uint64_t now;
+    /* When to wake up next */
+    uint64_t timer_time = INT64_MAX;
+    /* List of events to emit */
+    QTAILQ_HEAD(, DelayEvent) delay_events =
+        QTAILQ_HEAD_INITIALIZER(delay_events);
+
+    WITH_QEMU_LOCK_GUARD(&bdt->lock) {
+        now = qemu_clock_get_ns(bdt->clock_type);
+
+        for (int i = 0; i < BLOCK_DELAY__MAX; i++) {
+            BlockDelayTrackerRequest *request;
+            uint64_t threshold_ns = bdt->threshold_ns[i];
+
+            if (!threshold_ns) {
+                continue;
+            }
+
+            /*
+             * Sleep at most for the duration of this threshold, so that we
+             * will regularly check new requests as they come in.
+             */
+            timer_time = MIN(timer_time, uadd64_saturate(now, threshold_ns));
+
+            QTAILQ_FOREACH(request, &bdt->requests, list) {
+                DelayEvent *evt;
+                char *req_dev_path;
+                uint64_t duration_ns;
+
+                /*
+                 * New requests are entered at the end of the list, so it
+                 * is always sorted by start_time_ns (ascending).  We can
+                 * stop iterating on the first request that is not yet older
+                 * than the delay threshold.
+                 */
+                duration_ns = usub64_saturate(now, request->start_time_ns);
+                if (duration_ns < threshold_ns) {
+                    /*
+                     * Ensure waking when we need to emit a delay event for
+                     * this request, the one closest to the delay threshold.
+                     */
+                    timer_time = MIN(timer_time,
+                                     uadd64_saturate(request->start_time_ns,
+                                                     threshold_ns));
+                    break;
+                }
+
+                /* Only emit a delay event once */
+                if (request->threshold_exceeded[i]) {
+                    continue;
+                }
+
+                /*
+                 * Always use the same device path for a single request, so use
+                 * the cached path if it exists already.  Otherwise, use the
+                 * current device path.
+                 */
+                req_dev_path = request->device_path ?: dev_path;
+                if (!req_dev_path) {
+                    assert(!request->device_path && !dev_path);
+
+                    /*
+                     * Fine to call under `BlockDelayTracker.lock` because this
+                     * will not take a lock itself.
+                     * The alternative would be to either call it before the
+                     * lock (not yet knowing if there will be delayed requests),
+                     * or call it after, at which point `request` may no longer
+                     * be alive, which would disallow putting the result into
+                     * `request`. We want to put it into `request`, though, so
+                     * the `BLOCK_IO_DONE` event will always emit the same path
+                     * as the `BLOCK_IO_DELAY` event(s) before.
+                     * So, in order to call `blk_get_attached_dev_path()` only
+                     * when needed and to make `request->device_path` work as a
+                     * per-request cache, we have to call it under the lock.
+                     */
+                    req_dev_path = dev_path = blk_get_attached_dev_path(blk);
+                }
+
+                evt = g_new(DelayEvent, 1);
+                *evt = (DelayEvent) {
+                    .device_path = g_strdup(req_dev_path),
+                    .op_type = request->op_type,
+                    .duration_ns = duration_ns,
+                    .action = i == BLOCK_DELAY_REPORT
+                        ? BLOCK_ERROR_ACTION_REPORT
+                        : BLOCK_ERROR_ACTION_STOP,
+                    .has_offset = request->offset >= 0,
+                    .offset = request->offset,
+                    .bytes = request->bytes,
+                };
+                QTAILQ_INSERT_TAIL(&delay_events, evt, next);
+
+                /* For BLOCK_IO_DONE */
+                if (!request->device_path) {
+                    request->device_path = g_strdup(req_dev_path);
+                }
+                request->threshold_exceeded[i] = true;
+
+                if (i == BLOCK_DELAY_STOP) {
+                    stop_vm = true;
+                }
+            }
+        }
+    }
+
+    if (stop_vm) {
+        qemu_system_vmstop_request_prepare();
+    }
+    while (!QTAILQ_EMPTY(&delay_events)) {
+        DelayEvent *evt = QTAILQ_FIRST(&delay_events);
+        QTAILQ_REMOVE(&delay_events, evt, next);
+
+        qapi_event_send_block_io_delay(evt->device_path, evt->op_type,
+                                       evt->duration_ns, evt->action,
+                                       evt->has_offset, evt->offset,
+                                       evt->bytes);
+        g_free(evt->device_path);
+        g_free(evt);
+    }
+    if (stop_vm) {
+        qemu_system_vmstop_request(RUN_STATE_IO_HANG);
+    }
+
+    if (timer_time < INT64_MAX) {
+        timer_mod_ns(bdt->timer, timer_time);
+    }
+}
diff --git a/block/block-backend.c b/block/block-backend.c
index 78678986466..badc3986f9e 100644
--- a/block/block-backend.c
+++ b/block/block-backend.c
@@ -12,6 +12,7 @@
 
 #include "qemu/osdep.h"
 #include "system/block-backend.h"
+#include "block/block-backend-delay.h"
 #include "block/block_int.h"
 #include "block/blockjob.h"
 #include "block/coroutines.h"
@@ -93,6 +94,8 @@ struct BlockBackend {
      * Accessed with atomic ops.
      */
     unsigned int in_flight;
+
+    BlockDelayTracker *delay_tracker;
 };
 
 typedef struct BlockBackendAIOCB {
@@ -377,6 +380,9 @@ BlockBackend *blk_new(AioContext *ctx, uint64_t perm, uint64_t shared_perm)
     notifier_list_init(&blk->insert_bs_notifiers);
     QLIST_INIT(&blk->aio_notifiers);
 
+    blk->delay_tracker = g_new0(BlockDelayTracker, 1);
+    blk_delay_tracker_init(blk);
+
     QTAILQ_INSERT_TAIL(&block_backends, blk, link);
     return blk;
 }
@@ -481,6 +487,9 @@ static void blk_delete(BlockBackend *blk)
     assert(!blk->refcnt);
     assert(!blk->name);
     assert(!blk->dev);
+
+    blk_delay_tracker_destroy(blk->delay_tracker);
+
     if (blk->public.throttle_group_member.throttle_state) {
         blk_io_limits_disable(blk);
     }
@@ -764,6 +773,12 @@ BlockDriverState *blk_bs(BlockBackend *blk)
     return blk->root ? blk->root->bs : NULL;
 }
 
+BlockDelayTracker *blk_delay_tracker(BlockBackend *blk)
+{
+    IO_OR_GS_CODE();
+    return blk->delay_tracker;
+}
+
 static BlockBackend * GRAPH_RDLOCK bdrv_first_blk(BlockDriverState *bs)
 {
     BdrvChild *child;
@@ -1338,6 +1353,7 @@ blk_co_do_preadv_part(BlockBackend *blk, int64_t offset, int64_t bytes,
     ThrottleGroupMember *tgm = &blk->public.throttle_group_member;
     int ret;
     BlockDriverState *bs;
+    BlockDelayTrackerRequest request;
     IO_CODE();
 
     blk_wait_while_drained(blk, flags);
@@ -1353,6 +1369,8 @@ blk_co_do_preadv_part(BlockBackend *blk, int64_t offset, int64_t bytes,
     }
 
     bdrv_inc_in_flight(bs);
+    blk_track_delay_begin(blk, &request, offset, bytes,
+                          IO_ACCOUNTING_OPERATION_READ);
 
     /* throttling disk I/O */
     if (qatomic_load_acquire(&tgm->throttle_state)) {
@@ -1361,6 +1379,7 @@ blk_co_do_preadv_part(BlockBackend *blk, int64_t offset, int64_t bytes,
 
     ret = bdrv_co_preadv_part(blk->root, offset, bytes, qiov, qiov_offset,
                               flags);
+    blk_track_delay_end(blk, &request, ret);
     bdrv_dec_in_flight(bs);
     return ret;
 }
@@ -1413,6 +1432,7 @@ blk_co_do_pwritev_part(BlockBackend *blk, int64_t offset, int64_t bytes,
     ThrottleGroupMember *tgm = &blk->public.throttle_group_member;
     int ret;
     BlockDriverState *bs;
+    BlockDelayTrackerRequest request;
     IO_CODE();
 
     blk_wait_while_drained(blk, flags);
@@ -1428,6 +1448,8 @@ blk_co_do_pwritev_part(BlockBackend *blk, int64_t offset, int64_t bytes,
     }
 
     bdrv_inc_in_flight(bs);
+    blk_track_delay_begin(blk, &request, offset, bytes,
+                          IO_ACCOUNTING_OPERATION_WRITE);
     /* throttling disk I/O */
     if (qatomic_load_acquire(&tgm->throttle_state)) {
         throttle_group_co_io_limits_intercept(tgm, bytes, THROTTLE_WRITE);
@@ -1439,6 +1461,7 @@ blk_co_do_pwritev_part(BlockBackend *blk, int64_t offset, int64_t bytes,
 
     ret = bdrv_co_pwritev_part(blk->root, offset, bytes, qiov, qiov_offset,
                                flags);
+    blk_track_delay_end(blk, &request, ret);
     bdrv_dec_in_flight(bs);
     return ret;
 }
@@ -1803,6 +1826,7 @@ static int coroutine_fn
 blk_co_do_pdiscard(BlockBackend *blk, int64_t offset, int64_t bytes,
                    BdrvRequestFlags flags)
 {
+    BlockDelayTrackerRequest request;
     int ret;
     IO_CODE();
 
@@ -1814,7 +1838,12 @@ blk_co_do_pdiscard(BlockBackend *blk, int64_t offset, int64_t bytes,
         return ret;
     }
 
-    return bdrv_co_pdiscard(blk->root, offset, bytes);
+    blk_track_delay_begin(blk, &request, offset, bytes,
+                          IO_ACCOUNTING_OPERATION_UNMAP);
+    ret = bdrv_co_pdiscard(blk->root, offset, bytes);
+    blk_track_delay_end(blk, &request, ret);
+
+    return ret;
 }
 
 static void coroutine_fn blk_aio_pdiscard_entry(void *opaque)
@@ -1851,6 +1880,8 @@ int coroutine_fn blk_co_pdiscard(BlockBackend *blk, int64_t offset,
 /* To be called between exactly one pair of blk_inc/dec_in_flight() */
 static int coroutine_fn blk_co_do_flush(BlockBackend *blk)
 {
+    BlockDelayTrackerRequest request;
+    int ret;
     IO_CODE();
     blk_wait_while_drained(blk, 0);
     GRAPH_RDLOCK_GUARD();
@@ -1859,7 +1890,12 @@ static int coroutine_fn blk_co_do_flush(BlockBackend *blk)
         return -ENOMEDIUM;
     }
 
-    return bdrv_co_flush(blk_bs(blk));
+    blk_track_delay_begin(blk, &request, -1, 0,
+                          IO_ACCOUNTING_OPERATION_FLUSH);
+    ret = bdrv_co_flush(blk_bs(blk));
+    blk_track_delay_end(blk, &request, ret);
+
+    return ret;
 }
 
 static void coroutine_fn blk_aio_flush_entry(void *opaque)
@@ -2072,6 +2108,7 @@ int coroutine_fn blk_co_zone_mgmt(BlockBackend *blk, BlockZoneOp op,
 int coroutine_fn blk_co_zone_append(BlockBackend *blk, int64_t *offset,
         QEMUIOVector *qiov, BdrvRequestFlags flags)
 {
+    BlockDelayTrackerRequest request;
     int ret;
     IO_CODE();
 
@@ -2083,7 +2120,10 @@ int coroutine_fn blk_co_zone_append(BlockBackend *blk, int64_t *offset,
         return -ENOMEDIUM;
     }
 
+    blk_track_delay_begin(blk, &request, *offset, qiov->size,
+                          IO_ACCOUNTING_OPERATION_ZONE_APPEND);
     ret = bdrv_co_zone_append(blk_bs(blk), offset, qiov, flags);
+    blk_track_delay_end(blk, &request, ret);
     blk_dec_in_flight(blk);
     return ret;
 }
diff --git a/block/meson.build b/block/meson.build
index bc419aebf08..f758c82f3c9 100644
--- a/block/meson.build
+++ b/block/meson.build
@@ -8,6 +8,7 @@ block_ss.add(files(
   'blklogwrites.c',
   'blkverify.c',
   'block-backend.c',
+  'block-backend-delay.c',
   'block-copy.c',
   'commit.c',
   'copy-before-write.c',
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 18+ messages in thread

* [PATCH v2 11/12] block: Add delay-report-ns and delay-stop-ns props
  2026-10-19 18:20 [PATCH v2 00/12] block: BLOCK_IO_DELAY event Hanna Czenczek
                   ` (9 preceding siblings ...)
  2026-10-19 18:20 ` [PATCH v2 10/12] block: Per-request delay tracker Hanna Czenczek
@ 2026-10-19 18:20 ` Hanna Czenczek
  2026-10-19 18:20 ` [PATCH v2 12/12] iotests: Add io-delay-reporting test Hanna Czenczek
  11 siblings, 0 replies; 18+ messages in thread
From: Hanna Czenczek @ 2026-10-19 18:20 UTC (permalink / raw)
  To: qemu-block
  Cc: qemu-devel, Hanna Czenczek, Kevin Wolf, John Snow,
	Denis V . Lunev, Eric Blake, Markus Armbruster, Stefan Hajnoczi,
	Paolo Bonzini

These device properties allow setting up I/O latency thresholds for when
to report an I/O operation delay via QMP event (BLOCK_IO_DELAY) or when
to stop the VM.

Signed-off-by: Hanna Czenczek <hreitz@redhat.com>
---
 include/hw/block/block.h   |  8 +++-
 blockdev.c                 | 16 ++++++++
 hw/block/block.c           |  7 ++++
 tests/qemu-iotests/172.out | 76 ++++++++++++++++++++++++++++++++++++++
 4 files changed, 106 insertions(+), 1 deletion(-)

diff --git a/include/hw/block/block.h b/include/hw/block/block.h
index df941df19f2..3edc33a0339 100644
--- a/include/hw/block/block.h
+++ b/include/hw/block/block.h
@@ -36,6 +36,8 @@ typedef struct BlockConf {
     BlockdevOnError werror;
     uint32_t num_stats_intervals;
     uint32_t *stats_intervals;
+    uint64_t delay_report_ns;
+    uint64_t delay_stop_ns;
 } BlockConf;
 
 static inline unsigned int get_physical_block_exp(BlockConf *conf)
@@ -83,7 +85,11 @@ static inline unsigned int get_physical_block_exp(BlockConf *conf)
                             _conf.account_failed, ON_OFF_AUTO_AUTO),    \
     DEFINE_PROP_ARRAY("stats-intervals", _state,                        \
                      _conf.num_stats_intervals, _conf.stats_intervals,  \
-                     qdev_prop_uint32, uint32_t)
+                     qdev_prop_uint32, uint32_t),                       \
+    DEFINE_PROP_UINT64("delay-report-ns", _state, _conf.delay_report_ns,\
+                       0),                                              \
+    DEFINE_PROP_UINT64("delay-stop-ns", _state, _conf.delay_stop_ns,    \
+                       0)
 
 #define DEFINE_BLOCK_PROPERTIES(_state, _conf)                          \
     DEFINE_PROP_DRIVE("drive", _state, _conf.blk),                      \
diff --git a/blockdev.c b/blockdev.c
index 6e86c6262f9..90d94900645 100644
--- a/blockdev.c
+++ b/blockdev.c
@@ -496,6 +496,7 @@ static BlockBackend *blockdev_init(const char *file, QDict *bs_opts,
     BlockdevDetectZeroesOptions detect_zeroes =
         BLOCKDEV_DETECT_ZEROES_OPTIONS_OFF;
     const char *throttling_group = NULL;
+    uint64_t delay_report_ns, delay_stop_ns;
 
     /* Check common options by copying from bs_opts to opts, all other options
      * stay in bs_opts for processing by bdrv_open(). */
@@ -580,6 +581,9 @@ static BlockBackend *blockdev_init(const char *file, QDict *bs_opts,
 
     read_only = qemu_opt_get_bool(opts, BDRV_OPT_READ_ONLY, false);
 
+    delay_report_ns = qemu_opt_get_number(opts, "delay-report-ns", 0);
+    delay_stop_ns = qemu_opt_get_number(opts, "delay-stop-ns", 0);
+
     /* init */
     if ((!file || !*file) && !qdict_size(bs_opts)) {
         BlockBackendRootState *blk_rs;
@@ -615,6 +619,8 @@ static BlockBackend *blockdev_init(const char *file, QDict *bs_opts,
         }
         bs = blk_bs(blk);
 
+        blk_set_delay_report_threshold(blk, delay_report_ns);
+        blk_set_delay_stop_threshold(blk, delay_stop_ns);
         bs->detect_zeroes = detect_zeroes;
 
         block_acct_setup(blk_get_stats(blk), account_invalid, account_failed,
@@ -3753,6 +3759,16 @@ QemuOptsList qemu_common_drive_opts = {
             .type = QEMU_OPT_BOOL,
             .help = "whether to account for failed I/O operations "
                     "in the statistics",
+        },{
+            .name = "delay-report-ns",
+            .type = QEMU_OPT_NUMBER,
+            .help = "threshold in ns when to report an I/O operation "
+                    "delay via QMP event",
+        },{
+            .name = "delay-stop-ns",
+            .type = QEMU_OPT_NUMBER,
+            .help = "threshold in ns when to pause the VM because "
+                    "of an I/O operation delay",
         },
         { /* end of list */ }
     },
diff --git a/hw/block/block.c b/hw/block/block.c
index f187fa025d0..82d6f2e7980 100644
--- a/hw/block/block.c
+++ b/hw/block/block.c
@@ -249,6 +249,13 @@ bool blkconf_apply_backend_options(BlockConf *conf, bool readonly,
     blk_set_enable_write_cache(blk, wce);
     blk_set_on_error(blk, rerror, werror);
 
+    if (conf->delay_report_ns) {
+        blk_set_delay_report_threshold(blk, conf->delay_report_ns);
+    }
+    if (conf->delay_stop_ns) {
+        blk_set_delay_stop_threshold(blk, conf->delay_stop_ns);
+    }
+
     if (!block_acct_setup(blk_get_stats(blk), conf->account_invalid,
                           conf->account_failed, conf->stats_intervals,
                           conf->num_stats_intervals, errp)) {
diff --git a/tests/qemu-iotests/172.out b/tests/qemu-iotests/172.out
index a023cd407de..97ba78a8d53 100644
--- a/tests/qemu-iotests/172.out
+++ b/tests/qemu-iotests/172.out
@@ -31,6 +31,8 @@ Testing:
                 account-invalid = "auto"
                 account-failed = "auto"
                 stats-intervals = <null>
+                delay-report-ns = 0 (0x0)
+                delay-stop-ns = 0 (0x0)
                 drive-type = "288"
 
 
@@ -61,6 +63,8 @@ Testing: -fda TEST_DIR/t.qcow2
                 account-invalid = "auto"
                 account-failed = "auto"
                 stats-intervals = <null>
+                delay-report-ns = 0 (0x0)
+                delay-stop-ns = 0 (0x0)
                 drive-type = "144"
 floppy0 (NODE_NAME): TEST_DIR/t.qcow2 (qcow2)
     Attached to:      /machine/unattached/device[N]
@@ -98,6 +102,8 @@ Testing: -fdb TEST_DIR/t.qcow2
                 account-invalid = "auto"
                 account-failed = "auto"
                 stats-intervals = <null>
+                delay-report-ns = 0 (0x0)
+                delay-stop-ns = 0 (0x0)
                 drive-type = "144"
               dev: floppy, id ""
                 unit = 0 (0x0)
@@ -113,6 +119,8 @@ Testing: -fdb TEST_DIR/t.qcow2
                 account-invalid = "auto"
                 account-failed = "auto"
                 stats-intervals = <null>
+                delay-report-ns = 0 (0x0)
+                delay-stop-ns = 0 (0x0)
                 drive-type = "288"
 floppy1 (NODE_NAME): TEST_DIR/t.qcow2 (qcow2)
     Attached to:      /machine/unattached/device[N]
@@ -154,6 +162,8 @@ Testing: -fda TEST_DIR/t.qcow2 -fdb TEST_DIR/t.qcow2.2
                 account-invalid = "auto"
                 account-failed = "auto"
                 stats-intervals = <null>
+                delay-report-ns = 0 (0x0)
+                delay-stop-ns = 0 (0x0)
                 drive-type = "144"
               dev: floppy, id ""
                 unit = 0 (0x0)
@@ -169,6 +179,8 @@ Testing: -fda TEST_DIR/t.qcow2 -fdb TEST_DIR/t.qcow2.2
                 account-invalid = "auto"
                 account-failed = "auto"
                 stats-intervals = <null>
+                delay-report-ns = 0 (0x0)
+                delay-stop-ns = 0 (0x0)
                 drive-type = "144"
 floppy0 (NODE_NAME): TEST_DIR/t.qcow2 (qcow2)
     Attached to:      /machine/unattached/device[N]
@@ -211,6 +223,8 @@ Testing: -fdb
                 account-invalid = "auto"
                 account-failed = "auto"
                 stats-intervals = <null>
+                delay-report-ns = 0 (0x0)
+                delay-stop-ns = 0 (0x0)
                 drive-type = "288"
               dev: floppy, id ""
                 unit = 0 (0x0)
@@ -226,6 +240,8 @@ Testing: -fdb
                 account-invalid = "auto"
                 account-failed = "auto"
                 stats-intervals = <null>
+                delay-report-ns = 0 (0x0)
+                delay-stop-ns = 0 (0x0)
                 drive-type = "288"
 
 
@@ -256,6 +272,8 @@ Testing: -drive if=floppy,file=TEST_DIR/t.qcow2
                 account-invalid = "auto"
                 account-failed = "auto"
                 stats-intervals = <null>
+                delay-report-ns = 0 (0x0)
+                delay-stop-ns = 0 (0x0)
                 drive-type = "144"
 floppy0 (NODE_NAME): TEST_DIR/t.qcow2 (qcow2)
     Attached to:      /machine/unattached/device[N]
@@ -293,6 +311,8 @@ Testing: -drive if=floppy,file=TEST_DIR/t.qcow2,index=1
                 account-invalid = "auto"
                 account-failed = "auto"
                 stats-intervals = <null>
+                delay-report-ns = 0 (0x0)
+                delay-stop-ns = 0 (0x0)
                 drive-type = "144"
               dev: floppy, id ""
                 unit = 0 (0x0)
@@ -308,6 +328,8 @@ Testing: -drive if=floppy,file=TEST_DIR/t.qcow2,index=1
                 account-invalid = "auto"
                 account-failed = "auto"
                 stats-intervals = <null>
+                delay-report-ns = 0 (0x0)
+                delay-stop-ns = 0 (0x0)
                 drive-type = "288"
 floppy1 (NODE_NAME): TEST_DIR/t.qcow2 (qcow2)
     Attached to:      /machine/unattached/device[N]
@@ -349,6 +371,8 @@ Testing: -drive if=floppy,file=TEST_DIR/t.qcow2 -drive if=floppy,file=TEST_DIR/t
                 account-invalid = "auto"
                 account-failed = "auto"
                 stats-intervals = <null>
+                delay-report-ns = 0 (0x0)
+                delay-stop-ns = 0 (0x0)
                 drive-type = "144"
               dev: floppy, id ""
                 unit = 0 (0x0)
@@ -364,6 +388,8 @@ Testing: -drive if=floppy,file=TEST_DIR/t.qcow2 -drive if=floppy,file=TEST_DIR/t
                 account-invalid = "auto"
                 account-failed = "auto"
                 stats-intervals = <null>
+                delay-report-ns = 0 (0x0)
+                delay-stop-ns = 0 (0x0)
                 drive-type = "144"
 floppy0 (NODE_NAME): TEST_DIR/t.qcow2 (qcow2)
     Attached to:      /machine/unattached/device[N]
@@ -409,6 +435,8 @@ Testing: -drive if=none,file=TEST_DIR/t.qcow2 -device floppy,drive=none0
                 account-invalid = "auto"
                 account-failed = "auto"
                 stats-intervals = <null>
+                delay-report-ns = 0 (0x0)
+                delay-stop-ns = 0 (0x0)
                 drive-type = "144"
 none0 (NODE_NAME): TEST_DIR/t.qcow2 (qcow2)
     Attached to:      /machine/peripheral-anon/device[N]
@@ -446,6 +474,8 @@ Testing: -drive if=none,file=TEST_DIR/t.qcow2 -device floppy,drive=none0,unit=1
                 account-invalid = "auto"
                 account-failed = "auto"
                 stats-intervals = <null>
+                delay-report-ns = 0 (0x0)
+                delay-stop-ns = 0 (0x0)
                 drive-type = "144"
 none0 (NODE_NAME): TEST_DIR/t.qcow2 (qcow2)
     Attached to:      /machine/peripheral-anon/device[N]
@@ -483,6 +513,8 @@ Testing: -drive if=none,file=TEST_DIR/t.qcow2 -drive if=none,file=TEST_DIR/t.qco
                 account-invalid = "auto"
                 account-failed = "auto"
                 stats-intervals = <null>
+                delay-report-ns = 0 (0x0)
+                delay-stop-ns = 0 (0x0)
                 drive-type = "144"
               dev: floppy, id ""
                 unit = 0 (0x0)
@@ -498,6 +530,8 @@ Testing: -drive if=none,file=TEST_DIR/t.qcow2 -drive if=none,file=TEST_DIR/t.qco
                 account-invalid = "auto"
                 account-failed = "auto"
                 stats-intervals = <null>
+                delay-report-ns = 0 (0x0)
+                delay-stop-ns = 0 (0x0)
                 drive-type = "144"
 none0 (NODE_NAME): TEST_DIR/t.qcow2 (qcow2)
     Attached to:      /machine/peripheral-anon/device[N]
@@ -549,6 +583,8 @@ Testing: -fda TEST_DIR/t.qcow2 -drive if=none,file=TEST_DIR/t.qcow2.2 -device fl
                 account-invalid = "auto"
                 account-failed = "auto"
                 stats-intervals = <null>
+                delay-report-ns = 0 (0x0)
+                delay-stop-ns = 0 (0x0)
                 drive-type = "144"
               dev: floppy, id ""
                 unit = 0 (0x0)
@@ -564,6 +600,8 @@ Testing: -fda TEST_DIR/t.qcow2 -drive if=none,file=TEST_DIR/t.qcow2.2 -device fl
                 account-invalid = "auto"
                 account-failed = "auto"
                 stats-intervals = <null>
+                delay-report-ns = 0 (0x0)
+                delay-stop-ns = 0 (0x0)
                 drive-type = "144"
 floppy0 (NODE_NAME): TEST_DIR/t.qcow2 (qcow2)
     Attached to:      /machine/unattached/device[N]
@@ -606,6 +644,8 @@ Testing: -fda TEST_DIR/t.qcow2 -drive if=none,file=TEST_DIR/t.qcow2.2 -device fl
                 account-invalid = "auto"
                 account-failed = "auto"
                 stats-intervals = <null>
+                delay-report-ns = 0 (0x0)
+                delay-stop-ns = 0 (0x0)
                 drive-type = "144"
               dev: floppy, id ""
                 unit = 0 (0x0)
@@ -621,6 +661,8 @@ Testing: -fda TEST_DIR/t.qcow2 -drive if=none,file=TEST_DIR/t.qcow2.2 -device fl
                 account-invalid = "auto"
                 account-failed = "auto"
                 stats-intervals = <null>
+                delay-report-ns = 0 (0x0)
+                delay-stop-ns = 0 (0x0)
                 drive-type = "144"
 floppy0 (NODE_NAME): TEST_DIR/t.qcow2 (qcow2)
     Attached to:      /machine/unattached/device[N]
@@ -663,6 +705,8 @@ Testing: -fdb TEST_DIR/t.qcow2 -drive if=none,file=TEST_DIR/t.qcow2.2 -device fl
                 account-invalid = "auto"
                 account-failed = "auto"
                 stats-intervals = <null>
+                delay-report-ns = 0 (0x0)
+                delay-stop-ns = 0 (0x0)
                 drive-type = "144"
               dev: floppy, id ""
                 unit = 1 (0x1)
@@ -678,6 +722,8 @@ Testing: -fdb TEST_DIR/t.qcow2 -drive if=none,file=TEST_DIR/t.qcow2.2 -device fl
                 account-invalid = "auto"
                 account-failed = "auto"
                 stats-intervals = <null>
+                delay-report-ns = 0 (0x0)
+                delay-stop-ns = 0 (0x0)
                 drive-type = "144"
 floppy1 (NODE_NAME): TEST_DIR/t.qcow2 (qcow2)
     Attached to:      /machine/unattached/device[N]
@@ -720,6 +766,8 @@ Testing: -fdb TEST_DIR/t.qcow2 -drive if=none,file=TEST_DIR/t.qcow2.2 -device fl
                 account-invalid = "auto"
                 account-failed = "auto"
                 stats-intervals = <null>
+                delay-report-ns = 0 (0x0)
+                delay-stop-ns = 0 (0x0)
                 drive-type = "144"
               dev: floppy, id ""
                 unit = 1 (0x1)
@@ -735,6 +783,8 @@ Testing: -fdb TEST_DIR/t.qcow2 -drive if=none,file=TEST_DIR/t.qcow2.2 -device fl
                 account-invalid = "auto"
                 account-failed = "auto"
                 stats-intervals = <null>
+                delay-report-ns = 0 (0x0)
+                delay-stop-ns = 0 (0x0)
                 drive-type = "144"
 floppy1 (NODE_NAME): TEST_DIR/t.qcow2 (qcow2)
     Attached to:      /machine/unattached/device[N]
@@ -786,6 +836,8 @@ Testing: -drive if=floppy,file=TEST_DIR/t.qcow2 -drive if=none,file=TEST_DIR/t.q
                 account-invalid = "auto"
                 account-failed = "auto"
                 stats-intervals = <null>
+                delay-report-ns = 0 (0x0)
+                delay-stop-ns = 0 (0x0)
                 drive-type = "144"
               dev: floppy, id ""
                 unit = 0 (0x0)
@@ -801,6 +853,8 @@ Testing: -drive if=floppy,file=TEST_DIR/t.qcow2 -drive if=none,file=TEST_DIR/t.q
                 account-invalid = "auto"
                 account-failed = "auto"
                 stats-intervals = <null>
+                delay-report-ns = 0 (0x0)
+                delay-stop-ns = 0 (0x0)
                 drive-type = "144"
 floppy0 (NODE_NAME): TEST_DIR/t.qcow2 (qcow2)
     Attached to:      /machine/unattached/device[N]
@@ -843,6 +897,8 @@ Testing: -drive if=floppy,file=TEST_DIR/t.qcow2 -drive if=none,file=TEST_DIR/t.q
                 account-invalid = "auto"
                 account-failed = "auto"
                 stats-intervals = <null>
+                delay-report-ns = 0 (0x0)
+                delay-stop-ns = 0 (0x0)
                 drive-type = "144"
               dev: floppy, id ""
                 unit = 0 (0x0)
@@ -858,6 +914,8 @@ Testing: -drive if=floppy,file=TEST_DIR/t.qcow2 -drive if=none,file=TEST_DIR/t.q
                 account-invalid = "auto"
                 account-failed = "auto"
                 stats-intervals = <null>
+                delay-report-ns = 0 (0x0)
+                delay-stop-ns = 0 (0x0)
                 drive-type = "144"
 floppy0 (NODE_NAME): TEST_DIR/t.qcow2 (qcow2)
     Attached to:      /machine/unattached/device[N]
@@ -906,6 +964,8 @@ Testing: -drive if=none,file=TEST_DIR/t.qcow2 -global floppy.drive=none0 -device
                 account-invalid = "auto"
                 account-failed = "auto"
                 stats-intervals = <null>
+                delay-report-ns = 0 (0x0)
+                delay-stop-ns = 0 (0x0)
                 drive-type = "144"
 none0 (NODE_NAME): TEST_DIR/t.qcow2 (qcow2)
     Attached to:      /machine/peripheral-anon/device[N]
@@ -973,6 +1033,8 @@ Testing: -device floppy
                 account-invalid = "auto"
                 account-failed = "auto"
                 stats-intervals = <null>
+                delay-report-ns = 0 (0x0)
+                delay-stop-ns = 0 (0x0)
                 drive-type = "288"
 
 Testing: -device floppy,drive-type=120
@@ -1000,6 +1062,8 @@ Testing: -device floppy,drive-type=120
                 account-invalid = "auto"
                 account-failed = "auto"
                 stats-intervals = <null>
+                delay-report-ns = 0 (0x0)
+                delay-stop-ns = 0 (0x0)
                 drive-type = "120"
 
 Testing: -device floppy,drive-type=144
@@ -1027,6 +1091,8 @@ Testing: -device floppy,drive-type=144
                 account-invalid = "auto"
                 account-failed = "auto"
                 stats-intervals = <null>
+                delay-report-ns = 0 (0x0)
+                delay-stop-ns = 0 (0x0)
                 drive-type = "144"
 
 Testing: -device floppy,drive-type=288
@@ -1054,6 +1120,8 @@ Testing: -device floppy,drive-type=288
                 account-invalid = "auto"
                 account-failed = "auto"
                 stats-intervals = <null>
+                delay-report-ns = 0 (0x0)
+                delay-stop-ns = 0 (0x0)
                 drive-type = "288"
 
 
@@ -1084,6 +1152,8 @@ Testing: -drive if=none,file=TEST_DIR/t.qcow2 -device floppy,drive=none0,drive-t
                 account-invalid = "auto"
                 account-failed = "auto"
                 stats-intervals = <null>
+                delay-report-ns = 0 (0x0)
+                delay-stop-ns = 0 (0x0)
                 drive-type = "120"
 none0 (NODE_NAME): TEST_DIR/t.qcow2 (qcow2)
     Attached to:      /machine/peripheral-anon/device[N]
@@ -1121,6 +1191,8 @@ Testing: -drive if=none,file=TEST_DIR/t.qcow2 -device floppy,drive=none0,drive-t
                 account-invalid = "auto"
                 account-failed = "auto"
                 stats-intervals = <null>
+                delay-report-ns = 0 (0x0)
+                delay-stop-ns = 0 (0x0)
                 drive-type = "288"
 none0 (NODE_NAME): TEST_DIR/t.qcow2 (qcow2)
     Attached to:      /machine/peripheral-anon/device[N]
@@ -1161,6 +1233,8 @@ Testing: -drive if=none,file=TEST_DIR/t.qcow2 -device floppy,drive=none0,logical
                 account-invalid = "auto"
                 account-failed = "auto"
                 stats-intervals = <null>
+                delay-report-ns = 0 (0x0)
+                delay-stop-ns = 0 (0x0)
                 drive-type = "144"
 none0 (NODE_NAME): TEST_DIR/t.qcow2 (qcow2)
     Attached to:      /machine/peripheral-anon/device[N]
@@ -1198,6 +1272,8 @@ Testing: -drive if=none,file=TEST_DIR/t.qcow2 -device floppy,drive=none0,physica
                 account-invalid = "auto"
                 account-failed = "auto"
                 stats-intervals = <null>
+                delay-report-ns = 0 (0x0)
+                delay-stop-ns = 0 (0x0)
                 drive-type = "144"
 none0 (NODE_NAME): TEST_DIR/t.qcow2 (qcow2)
     Attached to:      /machine/peripheral-anon/device[N]
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 18+ messages in thread

* [PATCH v2 12/12] iotests: Add io-delay-reporting test
  2026-10-19 18:20 [PATCH v2 00/12] block: BLOCK_IO_DELAY event Hanna Czenczek
                   ` (10 preceding siblings ...)
  2026-10-19 18:20 ` [PATCH v2 11/12] block: Add delay-report-ns and delay-stop-ns props Hanna Czenczek
@ 2026-10-19 18:20 ` Hanna Czenczek
  11 siblings, 0 replies; 18+ messages in thread
From: Hanna Czenczek @ 2026-10-19 18:20 UTC (permalink / raw)
  To: qemu-block
  Cc: qemu-devel, Hanna Czenczek, Kevin Wolf, John Snow,
	Denis V . Lunev, Eric Blake, Markus Armbruster, Stefan Hajnoczi,
	Paolo Bonzini

Test delay-report-ns and delay-stop-ns via HMP qemu-io (break, aio_read,
resume) on a null node (with blkdebug).

Signed-off-by: Hanna Czenczek <hreitz@redhat.com>
---
 tests/qemu-iotests/tests/io-delay-reporting   | 187 ++++++++++++++++++
 .../qemu-iotests/tests/io-delay-reporting.out |  90 +++++++++
 2 files changed, 277 insertions(+)
 create mode 100755 tests/qemu-iotests/tests/io-delay-reporting
 create mode 100644 tests/qemu-iotests/tests/io-delay-reporting.out

diff --git a/tests/qemu-iotests/tests/io-delay-reporting b/tests/qemu-iotests/tests/io-delay-reporting
new file mode 100755
index 00000000000..7a51d2c4063
--- /dev/null
+++ b/tests/qemu-iotests/tests/io-delay-reporting
@@ -0,0 +1,187 @@
+#!/usr/bin/env python3
+# group: rw
+#
+# Test the I/O delay QMP event.
+#
+# Copyright (C) 2026 Red Hat, Inc.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+# Creator/Owner: Hanna Czenczek <hreitz@redhat.com>
+
+import asyncio
+import iotests
+from iotests import filter_qmp_event, log
+
+DELAY_REPORT_MS = 5000 # ms
+DELAY_STOP_MS = 10000 # ms
+EPSILON_MS = 50 # ms -- small delta that can safely be added/subtracted
+NS_PER_MS = 1000 * 1000
+DELAY_REPORT_NS = DELAY_REPORT_MS * NS_PER_MS
+DELAY_STOP_NS = DELAY_STOP_MS * NS_PER_MS
+
+qtest_clock = 0 # ms
+
+iotests.script_initialize()
+
+def vm_qemu_io(qvm, virtio_blk_id, cmd):
+    log(f'[HMP] qemu-io to {virtio_blk_id}: {cmd}')
+    command_line=f'qemu-io -d {virtio_blk_id}/virtio-backend "{cmd}"'
+    log(qvm.qmp('human-monitor-command', command_line=command_line))
+
+def advance_clock_to(qvm, clock_ms):
+    """
+    Retain the current absolute clock time so we can step *to* a specific time
+    via `clock_step`.
+    """
+
+    # pylint does not like global much, but this is a test script, and not
+    # using global would just make this more complicated.
+    global qtest_clock # pylint: disable=global-statement
+    msecs = clock_ms - qtest_clock
+    log(f'[qtest] clock_step {msecs}ms to {clock_ms}ms')
+    qvm.qtest(f'clock_step {msecs * NS_PER_MS}')
+    qtest_clock += msecs
+
+def reset_clock():
+    """
+    Reset the internal qtest clock to 0; use before launching a new VM.
+    """
+
+    global qtest_clock # pylint: disable=global-statement
+    qtest_clock = 0
+
+def await_event(qvm, match):
+    evt = qvm.event_wait(match, timeout=30.0)
+    assert evt is not None
+    return evt
+
+def assert_no_event(qvm, event_name):
+    try:
+        evt = qvm.event_wait(event_name, timeout=0.1)
+        assert evt is None, f'Unexpected {event_name} event: {evt}'
+    except asyncio.TimeoutError:
+        pass
+    log(f'(No {event_name} event)')
+
+def run(error):
+    """
+    If @error is False, run both requests normally.
+    If it is True, inject an error into the second one.
+    """
+
+    log('')
+    log(f'=== Running tests with error={error} ===')
+    log('')
+
+    reset_clock()
+    with iotests.VM() as vm:
+        # Cannot use null-co.latency-ns, as that uses realtime (not qtest time)
+        # Need to use raw to get the read_aio event working
+        opts = {
+            'driver': 'raw',
+            'node-name': 'test-node',
+            'file': {
+                'driver': 'blkdebug',
+                'image': {
+                    'driver': 'null-co',
+                },
+            },
+        }
+        if error:
+            opts['file']['inject-error'] = [{
+                'event': 'read_aio',
+                'iotype': 'read',
+                'sector': str(4096 // 512),
+                'once': 'on',
+            }]
+        vm.add_blockdev(vm.qmp_to_opts(opts))
+
+        vm.launch()
+
+        log(f'[QMP] device_add id=vblk delay-report-ns={DELAY_REPORT_NS} '
+            f'delay-stop-ns={DELAY_STOP_NS}')
+        log(vm.qmp('device_add', {
+            'driver': 'virtio-blk',
+            'id': 'vblk',
+            'drive': 'test-node',
+            'delay-report-ns': DELAY_REPORT_NS,
+            'delay-stop-ns': DELAY_STOP_NS,
+        }))
+
+        try:
+            vm_qemu_io(vm, 'vblk', 'break read_aio read_0')
+            vm_qemu_io(vm, 'vblk', 'aio_read 0k 4k')
+            vm_qemu_io(vm, 'vblk', 'wait_break read_0')
+
+            vm_qemu_io(vm, 'vblk', 'break read_aio read_1')
+            vm_qemu_io(vm, 'vblk', 'aio_read 4k 4k')
+            vm_qemu_io(vm, 'vblk', 'wait_break read_1')
+
+            advance_clock_to(vm, DELAY_REPORT_MS - EPSILON_MS)
+
+            # Assert there is no event before the step
+            assert_no_event(vm, 'BLOCK_IO_DELAY')
+
+            advance_clock_to(vm, DELAY_REPORT_MS)
+
+            # But now there should be, right on time
+            events = [await_event(vm, 'BLOCK_IO_DELAY'),
+                      await_event(vm, 'BLOCK_IO_DELAY')]
+            events.sort(key=lambda event: event['data']['offset'])
+            for event in events:
+                log(event, filters=[filter_qmp_event])
+
+            # Have the read_0 request finish -- advance clock to test the
+            # correct duration
+            advance_clock_to(vm, DELAY_REPORT_MS + EPSILON_MS)
+            vm_qemu_io(vm, 'vblk', 'resume read_0')
+            log(await_event(vm, 'BLOCK_IO_DONE'), filters=[filter_qmp_event])
+
+            # Let read_1 hang for longer to trigger a STOP
+            advance_clock_to(vm, DELAY_STOP_MS)
+
+            # Read the pre-STOP notification
+            log(await_event(vm, 'BLOCK_IO_DELAY'), filters=[filter_qmp_event])
+
+            # Then the actual STOP
+            log(await_event(vm, 'STOP'), filters=[filter_qmp_event])
+            log('[QMP] query-status')
+            log(vm.qmp('query-status'))
+
+            # Have the read_1 request finish
+            advance_clock_to(vm, DELAY_STOP_MS + EPSILON_MS)
+            vm_qemu_io(vm, 'vblk', 'resume read_1')
+            log(await_event(vm, 'BLOCK_IO_DONE'), filters=[filter_qmp_event])
+
+            log('[QMP] cont')
+            log(vm.qmp('cont'))
+            log(await_event(vm, 'RESUME'), filters=[filter_qmp_event])
+            log('[QMP] query-status')
+            log(vm.qmp('query-status'))
+        except Exception:
+            # The VM will not be able to quit without these requests finished,
+            # which (without this block) would make the test hang instead of
+            # exit if something unexpected goes wrong
+            vm_qemu_io(vm, 'vblk', 'resume read_0')
+            vm_qemu_io(vm, 'vblk', 'resume read_1')
+            raise
+
+        # Test device unplug to verify that stopping the delay tracking timer
+        # works
+        log('[QMP] device_del id=vblk')
+        log(vm.qmp('device_del', id='vblk'))
+
+        # system_reset forces the device unplug to happen
+        log('[QMP] system_reset')
+        log(vm.qmp('system_reset'))
+
+        event = vm.event_wait('DEVICE_DELETED', 20.0,
+                              {'data': {'device': 'vblk'}})
+        assert event is not None
+        log(event, filters=[filter_qmp_event])
+
+        vm.shutdown()
+
+run(False)
+run(True)
diff --git a/tests/qemu-iotests/tests/io-delay-reporting.out b/tests/qemu-iotests/tests/io-delay-reporting.out
new file mode 100644
index 00000000000..99349d2b124
--- /dev/null
+++ b/tests/qemu-iotests/tests/io-delay-reporting.out
@@ -0,0 +1,90 @@
+
+=== Running tests with error=False ===
+
+[QMP] device_add id=vblk delay-report-ns=5000000000 delay-stop-ns=10000000000
+{"return": {}}
+[HMP] qemu-io to vblk: break read_aio read_0
+{"return": ""}
+[HMP] qemu-io to vblk: aio_read 0k 4k
+{"return": ""}
+[HMP] qemu-io to vblk: wait_break read_0
+{"return": ""}
+[HMP] qemu-io to vblk: break read_aio read_1
+{"return": ""}
+[HMP] qemu-io to vblk: aio_read 4k 4k
+{"return": ""}
+[HMP] qemu-io to vblk: wait_break read_1
+{"return": ""}
+[qtest] clock_step 4950ms to 4950ms
+(No BLOCK_IO_DELAY event)
+[qtest] clock_step 50ms to 5000ms
+{"data": {"action": "report", "bytes": 4096, "latency-ns": 5000000000, "offset": 0, "operation": "read", "qom-path": "/machine/peripheral/vblk/virtio-backend"}, "event": "BLOCK_IO_DELAY", "timestamp": {"microseconds": "USECS", "seconds": "SECS"}}
+{"data": {"action": "report", "bytes": 4096, "latency-ns": 5000000000, "offset": 4096, "operation": "read", "qom-path": "/machine/peripheral/vblk/virtio-backend"}, "event": "BLOCK_IO_DELAY", "timestamp": {"microseconds": "USECS", "seconds": "SECS"}}
+[qtest] clock_step 50ms to 5050ms
+[HMP] qemu-io to vblk: resume read_0
+{"return": ""}
+{"data": {"bytes": 4096, "failed": false, "latency-ns": 5050000000, "offset": 0, "operation": "read", "qom-path": "/machine/peripheral/vblk/virtio-backend"}, "event": "BLOCK_IO_DONE", "timestamp": {"microseconds": "USECS", "seconds": "SECS"}}
+[qtest] clock_step 4950ms to 10000ms
+{"data": {"action": "stop", "bytes": 4096, "latency-ns": 10000000000, "offset": 4096, "operation": "read", "qom-path": "/machine/peripheral/vblk/virtio-backend"}, "event": "BLOCK_IO_DELAY", "timestamp": {"microseconds": "USECS", "seconds": "SECS"}}
+{"event": "STOP", "timestamp": {"microseconds": "USECS", "seconds": "SECS"}}
+[QMP] query-status
+{"return": {"running": false, "status": "io-hang"}}
+[qtest] clock_step 50ms to 10050ms
+[HMP] qemu-io to vblk: resume read_1
+{"return": ""}
+{"data": {"bytes": 4096, "failed": false, "latency-ns": 10050000000, "offset": 4096, "operation": "read", "qom-path": "/machine/peripheral/vblk/virtio-backend"}, "event": "BLOCK_IO_DONE", "timestamp": {"microseconds": "USECS", "seconds": "SECS"}}
+[QMP] cont
+{"return": {}}
+{"event": "RESUME", "timestamp": {"microseconds": "USECS", "seconds": "SECS"}}
+[QMP] query-status
+{"return": {"running": true, "status": "running"}}
+[QMP] device_del id=vblk
+{"return": {}}
+[QMP] system_reset
+{"return": {}}
+{"data": {"device": "vblk", "path": "/machine/peripheral/vblk"}, "event": "DEVICE_DELETED", "timestamp": {"microseconds": "USECS", "seconds": "SECS"}}
+
+=== Running tests with error=True ===
+
+[QMP] device_add id=vblk delay-report-ns=5000000000 delay-stop-ns=10000000000
+{"return": {}}
+[HMP] qemu-io to vblk: break read_aio read_0
+{"return": ""}
+[HMP] qemu-io to vblk: aio_read 0k 4k
+{"return": ""}
+[HMP] qemu-io to vblk: wait_break read_0
+{"return": ""}
+[HMP] qemu-io to vblk: break read_aio read_1
+{"return": ""}
+[HMP] qemu-io to vblk: aio_read 4k 4k
+{"return": ""}
+[HMP] qemu-io to vblk: wait_break read_1
+{"return": ""}
+[qtest] clock_step 4950ms to 4950ms
+(No BLOCK_IO_DELAY event)
+[qtest] clock_step 50ms to 5000ms
+{"data": {"action": "report", "bytes": 4096, "latency-ns": 5000000000, "offset": 0, "operation": "read", "qom-path": "/machine/peripheral/vblk/virtio-backend"}, "event": "BLOCK_IO_DELAY", "timestamp": {"microseconds": "USECS", "seconds": "SECS"}}
+{"data": {"action": "report", "bytes": 4096, "latency-ns": 5000000000, "offset": 4096, "operation": "read", "qom-path": "/machine/peripheral/vblk/virtio-backend"}, "event": "BLOCK_IO_DELAY", "timestamp": {"microseconds": "USECS", "seconds": "SECS"}}
+[qtest] clock_step 50ms to 5050ms
+[HMP] qemu-io to vblk: resume read_0
+{"return": ""}
+{"data": {"bytes": 4096, "failed": false, "latency-ns": 5050000000, "offset": 0, "operation": "read", "qom-path": "/machine/peripheral/vblk/virtio-backend"}, "event": "BLOCK_IO_DONE", "timestamp": {"microseconds": "USECS", "seconds": "SECS"}}
+[qtest] clock_step 4950ms to 10000ms
+{"data": {"action": "stop", "bytes": 4096, "latency-ns": 10000000000, "offset": 4096, "operation": "read", "qom-path": "/machine/peripheral/vblk/virtio-backend"}, "event": "BLOCK_IO_DELAY", "timestamp": {"microseconds": "USECS", "seconds": "SECS"}}
+{"event": "STOP", "timestamp": {"microseconds": "USECS", "seconds": "SECS"}}
+[QMP] query-status
+{"return": {"running": false, "status": "io-hang"}}
+[qtest] clock_step 50ms to 10050ms
+[HMP] qemu-io to vblk: resume read_1
+{"return": ""}
+{"data": {"bytes": 4096, "failed": true, "latency-ns": 10050000000, "offset": 4096, "operation": "read", "qom-path": "/machine/peripheral/vblk/virtio-backend"}, "event": "BLOCK_IO_DONE", "timestamp": {"microseconds": "USECS", "seconds": "SECS"}}
+[QMP] cont
+{"return": {}}
+{"event": "RESUME", "timestamp": {"microseconds": "USECS", "seconds": "SECS"}}
+[QMP] query-status
+{"return": {"running": true, "status": "running"}}
+[QMP] device_del id=vblk
+{"return": {}}
+[QMP] system_reset
+{"return": {}}
+{"data": {"device": "vblk", "path": "/machine/peripheral/vblk"}, "event": "DEVICE_DELETED", "timestamp": {"microseconds": "USECS", "seconds": "SECS"}}
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 18+ messages in thread

* Re: [PATCH v2 01/12] qapi/common: Clarify what IoOperationType is for
  2026-10-19 18:20 ` [PATCH v2 01/12] qapi/common: Clarify what IoOperationType is for Hanna Czenczek
@ 2026-10-20  5:58   ` Markus Armbruster
  0 siblings, 0 replies; 18+ messages in thread
From: Markus Armbruster @ 2026-10-20  5:58 UTC (permalink / raw)
  To: Hanna Czenczek
  Cc: qemu-block, qemu-devel, Kevin Wolf, John Snow, Denis V . Lunev,
	Eric Blake, Stefan Hajnoczi, Paolo Bonzini

Hanna Czenczek <hreitz@redhat.com> writes:

> The documentation for this type is a bit sparse right now, and
> specifically does not explain why it is limited to read/write only;
> "I/O operation type" could well imply this should distinguish a much
> broader range of operations like flushes, discards, etc., but it does
> not.
>
> Make a note why that is, which is that these types reflect the block
> device on-error policy settings that we have, which are rerror for read
> and werror for write.
>
> Signed-off-by: Hanna Czenczek <hreitz@redhat.com>
> ---
>  qapi/common.json | 5 ++++-
>  1 file changed, 4 insertions(+), 1 deletion(-)
>
> diff --git a/qapi/common.json b/qapi/common.json
> index 1352e664b23..880eb87a9cb 100644
> --- a/qapi/common.json
> +++ b/qapi/common.json
> @@ -9,7 +9,10 @@
>  
>  ##
>  # @IoOperationType:
> -#     An enumeration of the I/O operation types
> +#     An enumeration of the I/O operation types, limited to the block
> +#     device on-error policy settings we have (rerror and werror).
> +#     For reference, the corresponding error policy values are set in
> +#     `BlockdevOnError`.
>  #
>  # @read: read operation
>  #

I believe I get what you mean only because I read the cover letter
first.

My first attempt

    I/O operation types that may have a block device on-error policy.

is still confusing, because we use "block device" both for frontends and
backends.  I believe "block device on-error policy" refers to the qdev
properties "rerror" and "werror" certain block frontends / device models
have (virtio-blk-device, scsi-hd, scsi-cd, usb-storage).  Possible
clarification:

    These are the properties "rerror" and "werror" certain block device
    models have.

And then maybe

    @read: read operation, corresponding to property "rerror"

    @write: write operation, corresponding to property "werror"



^ permalink raw reply	[flat|nested] 18+ messages in thread

* Re: [PATCH v2 02/12] qapi/block: Add IoAccountingOperation enum
  2026-10-19 18:20 ` [PATCH v2 02/12] qapi/block: Add IoAccountingOperation enum Hanna Czenczek
@ 2026-10-20  5:59   ` Markus Armbruster
  0 siblings, 0 replies; 18+ messages in thread
From: Markus Armbruster @ 2026-10-20  5:59 UTC (permalink / raw)
  To: Hanna Czenczek
  Cc: qemu-block, qemu-devel, Kevin Wolf, John Snow, Denis V . Lunev,
	Eric Blake, Stefan Hajnoczi, Paolo Bonzini

Hanna Czenczek <hreitz@redhat.com> writes:

> This enum gives more information than IoOperationType (which is used for
> the rerror/werror distinction) to identify operation types for block
> accounting.
>
> Signed-off-by: Hanna Czenczek <hreitz@redhat.com>
> ---
>  qapi/block.json | 20 ++++++++++++++++++++
>  1 file changed, 20 insertions(+)
>
> diff --git a/qapi/block.json b/qapi/block.json
> index 0a88d405a26..8f4a71733c1 100644
> --- a/qapi/block.json
> +++ b/qapi/block.json
> @@ -629,3 +629,23 @@
>              'command': 'str' },
>    'features': [ 'unstable' ],
>    'allow-preconfig': true }
> +
> +##
> +# @IoAccountingOperation:
> +#
> +# I/O operation types as they are tracked by block accounting.

Can we have a link to suitable docs on block accounting?

> +#
> +# @read: Reading data
> +#
> +# @write: Writing data
> +#
> +# @flush: Flushing data to disk
> +#
> +# @zone-append: Appending data to a zone of a zoned block device
> +#
> +# @unmap: Discarding a block of data
> +#
> +# Since: 11.2
> +##
> +{ 'enum': 'IoAccountingOperation',
> +  'data': [ 'read', 'write', 'flush', 'zone-append', 'unmap' ] }



^ permalink raw reply	[flat|nested] 18+ messages in thread

* Re: [PATCH v2 05/12] qapi/run-state: Add io-hang state
  2026-10-19 18:20 ` [PATCH v2 05/12] qapi/run-state: Add io-hang state Hanna Czenczek
@ 2026-10-20  6:02   ` Markus Armbruster
  0 siblings, 0 replies; 18+ messages in thread
From: Markus Armbruster @ 2026-10-20  6:02 UTC (permalink / raw)
  To: Hanna Czenczek
  Cc: qemu-block, qemu-devel, Kevin Wolf, John Snow, Denis V . Lunev,
	Eric Blake, Markus Armbruster, Stefan Hajnoczi, Paolo Bonzini

Hanna Czenczek <hreitz@redhat.com> writes:

> This state is similar to io-error, but is not entered on I/O errors but
> instead when a request takes longer than a user-defined time (set via
> the delay-stop-ns property on a block device, which is not yet available
> but will be added by a future patch in this series).
>
> In contrast to other stop states, this state does not drain block
> devices.  That is because it is entered only when a request hangs, which
> means that draining would be counter-productive.
>
> Signed-off-by: Hanna Czenczek <hreitz@redhat.com>
> ---
>  qapi/run-state.json |  7 ++++++-
>  system/runstate.c   | 25 ++++++++++++++++++-------
>  2 files changed, 24 insertions(+), 8 deletions(-)
>
> diff --git a/qapi/run-state.json b/qapi/run-state.json
> index 8bbd9884087..cf3f43579de 100644
> --- a/qapi/run-state.json
> +++ b/qapi/run-state.json
> @@ -53,12 +53,17 @@
>  # @colo: guest is paused to save/restore VM state under colo
>  #     checkpoint, VM can not get into this state unless colo
>  #     capability is enabled for migration.  (since 2.8)
> +#
> +# @io-hang: An I/O operation has been delayed for too long and the
> +#     device is configured to stop the VM when that happens.  In
> +#     contrast to other paused states, the VM's block devices will not
> +#     be drained when entering this run state.  (since 11.2)
>  ##
>  { 'enum': 'RunState',
>    'data': [ 'debug', 'inmigrate', 'internal-error', 'io-error', 'paused',
>              'postmigrate', 'prelaunch', 'finish-migrate', 'restore-vm',
>              'running', 'save-vm', 'shutdown', 'suspended', 'watchdog',
> -            'guest-panicked', 'colo' ] }
> +            'guest-panicked', 'colo', 'io-hang' ] }
>  
>  ##
>  # @ShutdownCause:

QAPI schema
Acked-by: Markus Armbruster <armbru@redhat.com>



^ permalink raw reply	[flat|nested] 18+ messages in thread

* Re: [PATCH v2 06/12] qapi/block: Add BLOCK_IO_DELAY event
  2026-10-19 18:20 ` [PATCH v2 06/12] qapi/block: Add BLOCK_IO_DELAY event Hanna Czenczek
@ 2026-10-20  6:49   ` Markus Armbruster
  0 siblings, 0 replies; 18+ messages in thread
From: Markus Armbruster @ 2026-10-20  6:49 UTC (permalink / raw)
  To: Hanna Czenczek
  Cc: qemu-block, qemu-devel, Kevin Wolf, John Snow, Denis V . Lunev,
	Eric Blake, Stefan Hajnoczi, Paolo Bonzini

Hanna Czenczek <hreitz@redhat.com> writes:

> This event will be emitted when a disk operation takes longer than a
> pre-defined threshold (as set on the block device).
>
> Note that the description refers to the "delay-report-ns" and
> "delay-stop-ns" block device properties, which are not yet available.
> A future commit in this series will add them.
>
> Signed-off-by: Hanna Czenczek <hreitz@redhat.com>
> ---
>  qapi/block.json | 37 +++++++++++++++++++++++++++++++++++++
>  1 file changed, 37 insertions(+)
>
> diff --git a/qapi/block.json b/qapi/block.json
> index 8f4a71733c1..5f9f5c28050 100644
> --- a/qapi/block.json
> +++ b/qapi/block.json
> @@ -649,3 +649,40 @@
>  ##
>  { 'enum': 'IoAccountingOperation',
>    'data': [ 'read', 'write', 'flush', 'zone-append', 'unmap' ] }
> +
> +##
> +# @BLOCK_IO_DELAY:
> +#
> +# Emitted when a disk operation takes longer than the pre-defined
> +# threshold (as set via the "delay-report-ns" / "delay-stop-ns"
> +# properties on the block device).

As in PATCH 01, "block device" could be confusing, because we use the
term both for frontends and backends.  I believe "delay-report-ns" and
"delay-stop-ns" are the qdev properties every block device model will
have [PATCH 11].

> +#
> +# Note that delays deliberately incurred by throttling do count
> +# towards the operation duration.
> +#

Please use the revised syntax for introductions, like you did in PATCH
01:

   # @name: introduction, best to keep it brief, but you may continue it
   #     like this if necessary.  If your text need to refer to
   #     arguments, it's almost certainly not brief introduction
   #     anymore.
   #
   # Additional text can go here for now.

I apologize for the inconvenience.  We need this to eliminate certain
syntactic ambiguities that plagued the doc generator.  We'll need it
some more for the doc inliner.

> +# @qom-path: path to the block device object in the QOM tree
> +#
> +# @operation: I/O operation
> +#
> +# @latency-ns: Ongoing duration of the operation (in nanoseconds)
> +#
> +# @action: Action that has been taken ("report" for "delay-report-ns"
> +#     or "stop" for "delay-stop-ns").  A "stop" action means the VM
> +#     will be stopped with the "io-hang" `RunState`, which does not
> +#     drain the block devices attached to the VM, because draining
> +#     would be counterproductive in case of a hanging I/O request.

This feels less than clear.  What exactly happens?  My best guess:

* When an operation takes longer than delay-report-ns, BLOCK_IO_DELAY is
  emitted with "action": "report" (but only once for this operation).

* When an operation takes longer than delay-stop-ns, the guest is
  stopped (run state becomes io-hang), and BLOCK_IO_DELAY is emitted
  with "action": "stop".

  Say we continue the guest.  Can the even be triggered again for this
  operation?

The three possible values are ignore, report, and stop.  You cover
report and stop.  Can ignore happen?

We should use ``literal`` markup for literal values.

> +#
> +# @offset: Request offset in the guest disk (in bytes), if available
> +#
> +# @bytes: Request length
> +#
> +# Since: 11.2
> +##
> +{ 'event': 'BLOCK_IO_DELAY',
> +  'data': {
> +      'qom-path': 'str',
> +      'operation': 'IoAccountingOperation',
> +      'latency-ns': 'uint64',
> +      'action': 'BlockErrorAction',
> +      '*offset': 'uint64',
> +      'bytes': 'uint64' } }



^ permalink raw reply	[flat|nested] 18+ messages in thread

* Re: [PATCH v2 07/12] qapi/block: Add BLOCK_IO_DONE event
  2026-10-19 18:20 ` [PATCH v2 07/12] qapi/block: Add BLOCK_IO_DONE event Hanna Czenczek
@ 2026-10-20  6:55   ` Markus Armbruster
  0 siblings, 0 replies; 18+ messages in thread
From: Markus Armbruster @ 2026-10-20  6:55 UTC (permalink / raw)
  To: Hanna Czenczek
  Cc: qemu-block, qemu-devel, Kevin Wolf, John Snow, Denis V . Lunev,
	Eric Blake, Stefan Hajnoczi, Paolo Bonzini

Hanna Czenczek <hreitz@redhat.com> writes:

> This event is emitted when an operation completes, for which a
> BLOCK_IO_DELAY event has previously been emitted.
>
> Signed-off-by: Hanna Czenczek <hreitz@redhat.com>
> ---
>  qapi/block.json | 33 +++++++++++++++++++++++++++++++++
>  1 file changed, 33 insertions(+)
>
> diff --git a/qapi/block.json b/qapi/block.json
> index 5f9f5c28050..38f293dd32d 100644
> --- a/qapi/block.json
> +++ b/qapi/block.json
> @@ -660,6 +660,9 @@
>  # Note that delays deliberately incurred by throttling do count
>  # towards the operation duration.
>  #
> +# This event will only be emitted once per request and action.  When
> +# the operation does finish, a `BLOCK_IO_DONE` event is emitted.

I think the first sentence answers a question I had on the previous
patch.  Move it there?

> +#
>  # @qom-path: path to the block device object in the QOM tree
>  #
>  # @operation: I/O operation
> @@ -686,3 +689,33 @@
>        'action': 'BlockErrorAction',
>        '*offset': 'uint64',
>        'bytes': 'uint64' } }
> +
> +##
> +# @BLOCK_IO_DONE:
> +#
> +# Emitted on completion for disk operations that previously emitted a
> +# `BLOCK_IO_DELAY` event.

Can we have multiple operations in flight that previously emitted
BLOCK_IO_DELAY?

If yes, might a management application need to connect the BLOCK_IO_DONE
back to the BLOCK_IO_DELAY?

If yes, how could it do that?

Please use the revised syntax for introductions:

   ##
   # @BLOCK_IO_DONE: Emitted on completion for disk operations that
   #     previously emitted a `BLOCK_IO_DELAY` event.

> +#
> +# @qom-path: path to the block device object in the QOM tree
> +#
> +# @operation: I/O operation
> +#
> +# @latency-ns: Full duration of the operation (in nanoseconds)
> +#
> +# @offset: Request offset in the guest disk (in bytes), if available

Out of curiosity: when could @offset be unavailable?

> +#
> +# @bytes: Request length
> +#
> +# @failed: If true, this request failed for any reason.  If false, the

Suggest "for some reason".

> +#     request succeeded.
> +#
> +# Since: 11.2
> +##
> +{ 'event': 'BLOCK_IO_DONE',
> +  'data': {
> +      'qom-path': 'str',
> +      'operation': 'IoAccountingOperation',
> +      'latency-ns': 'uint64',
> +      '*offset': 'uint64',
> +      'bytes': 'uint64',
> +      'failed': 'bool' } }



^ permalink raw reply	[flat|nested] 18+ messages in thread

end of thread, other threads:[~2026-10-20  6:56 UTC | newest]

Thread overview: 18+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-19 18:20 [PATCH v2 00/12] block: BLOCK_IO_DELAY event Hanna Czenczek
2026-10-19 18:20 ` [PATCH v2 01/12] qapi/common: Clarify what IoOperationType is for Hanna Czenczek
2026-10-20  5:58   ` Markus Armbruster
2026-10-19 18:20 ` [PATCH v2 02/12] qapi/block: Add IoAccountingOperation enum Hanna Czenczek
2026-10-20  5:59   ` Markus Armbruster
2026-10-19 18:20 ` [PATCH v2 03/12] runstate: Allow IO_ERROR/SUSPENDED transitions Hanna Czenczek
2026-10-19 18:20 ` [PATCH v2 04/12] iotests: PC-only test for I/O errors during suspend Hanna Czenczek
2026-10-19 18:20 ` [PATCH v2 05/12] qapi/run-state: Add io-hang state Hanna Czenczek
2026-10-20  6:02   ` Markus Armbruster
2026-10-19 18:20 ` [PATCH v2 06/12] qapi/block: Add BLOCK_IO_DELAY event Hanna Czenczek
2026-10-20  6:49   ` Markus Armbruster
2026-10-19 18:20 ` [PATCH v2 07/12] qapi/block: Add BLOCK_IO_DONE event Hanna Czenczek
2026-10-20  6:55   ` Markus Armbruster
2026-10-19 18:20 ` [PATCH v2 08/12] block-backend: Public blk_get_attached_dev_path() Hanna Czenczek
2026-10-19 18:20 ` [PATCH v2 09/12] host-utils: Add u{add,sub}{32,64}_saturate() Hanna Czenczek
2026-10-19 18:20 ` [PATCH v2 10/12] block: Per-request delay tracker Hanna Czenczek
2026-10-19 18:20 ` [PATCH v2 11/12] block: Add delay-report-ns and delay-stop-ns props Hanna Czenczek
2026-10-19 18:20 ` [PATCH v2 12/12] iotests: Add io-delay-reporting test Hanna Czenczek

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.