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 <[email protected]>
---
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 <[email protected]>
+ *
+ * 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 <[email protected]>
+ *
+ * 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