On Fri, May 09, 2025 at 03:40:25PM -0500, Eric Blake wrote:
> QEMU has an optimization for a just-created drive-mirror destination
> that is not possible for blockdev-mirror (which can't create the
> destination) - any time we know the destination starts life as all
> zeroes, we can skip a pre-zeroing pass on the destination. Recent
> patches have added an improved heuristic for detecting if a file
> contains all zeroes, and we plan to use that heuristic in upcoming
> patches. But since a heuristic cannot quickly detect all scenarios,
> and there may be cases where the caller is aware of information that
> QEMU cannot learn quickly, it makes sense to have a way to tell QEMU
> to assume facts about the destination that can make the mirror
> operation faster. Given our existing example of "qemu-img convert
> --target-is-zero", it is time to expose this override in QMP for
> blockdev-mirror as well.
>
> This patch results in some slight redundancy between the older
> s->zero_target (set any time mode==FULL and the destination image was
> not just created - ie. clear if drive-mirror is asking to skip the
> pre-zero pass) and the newly-introduced s->target_is_zero (in addition
> to the QMP override, it is set when drive-mirror creates the
> destination image); this will be cleaned up in the next patch.
>
> There is also a subtlety that we must consider. When drive-mirror is
> passing target_is_zero on behalf of a just-created image, we know the
> image is sparse (skipping the pre-zeroing keeps it that way), so it
> doesn't matter whether the destination also has "discard":"unmap" and
> "detect-zeroes":"unmap". But now that we are letting the user set the
> knob for target-is-zero, if the user passes a pre-existing file that
> is fully allocated, it is fine to leave the file fully allocated under
> "detect-zeroes":"on", but if the file is open with
> "detect-zeroes":"unmap", we should really be trying harder to punch
> holes in the destination for every region of zeroes copied from the
> source. The easiest way to do this is to still run the pre-zeroing
> pass (turning the entire destination file sparse before populating
> just the allocated portions of the source), even though that currently
> results in double I/O to the portions of the file that are allocated.
> A later patch will add further optimizations to reduce redundant
> zeroing I/O during the mirror operation.
>
> Since "target-is-zero":true is designed for optimizations, it is okay
> to silently ignore the parameter rather than erroring if the user ever
> sets the parameter in a scenario where the mirror job can't exploit it
> (for example, when doing "sync":"top" instead of "sync":"full", we
> can't pre-zero, so setting the parameter won't make a speed
> difference).
>
> Signed-off-by: Eric Blake <[email protected]>
> Acked-by: Markus Armbruster <[email protected]>
>
> ---
>
> v4: hoist earlier in series. QMP design is unchanged, but logic in
> mirror_dirty_init is different (in many aspects simpler, but now
> catering to "detect-zeroes":"unmap") so Acked-by on QMP kept, but
> Reviewed-by dropped.
> ---
> qapi/block-core.json | 8 +++++++-
> include/block/block_int-global-state.h | 3 ++-
> block/mirror.c | 27 ++++++++++++++++++++++----
> blockdev.c | 18 ++++++++++-------
> tests/unit/test-block-iothread.c | 2 +-
> 5 files changed, 44 insertions(+), 14 deletions(-)
>
> diff --git a/qapi/block-core.json b/qapi/block-core.json
> index b1937780e19..7f70ec6d3cb 100644
> --- a/qapi/block-core.json
> +++ b/qapi/block-core.json
> @@ -2538,6 +2538,11 @@
> # disappear from the query list without user intervention.
> # Defaults to true. (Since 3.1)
> #
> +# @target-is-zero: Assume the destination reads as all zeroes before
> +# the mirror started. Setting this to true can speed up the
> +# mirror. Setting this to true when the destination is not
> +# actually all zero can corrupt the destination. (Since 10.1)
It appears feasible to add target-is-zero to the drive-mirror qmp as well,
provided this is done under the NEW_IMAGE_MODE_EXISTING scenario. This would
allow users to pass target-is-zero for optimizations whether using
blockdev-mirror
or drive-mirror when they have pre-prepared destination image.
> +#
> # Since: 2.6
> #
> # .. qmp-example::
> @@ -2557,7 +2562,8 @@
> '*on-target-error': 'BlockdevOnError',
> '*filter-node-name': 'str',
> '*copy-mode': 'MirrorCopyMode',
> - '*auto-finalize': 'bool', '*auto-dismiss': 'bool' },
> + '*auto-finalize': 'bool', '*auto-dismiss': 'bool',
> + '*target-is-zero': 'bool'},
> 'allow-preconfig': true }
>
> ##
> diff --git a/include/block/block_int-global-state.h
> b/include/block/block_int-global-state.h
> index eb2d92a2261..8cf0003ce78 100644
> --- a/include/block/block_int-global-state.h
> +++ b/include/block/block_int-global-state.h
> @@ -140,6 +140,7 @@ BlockJob *commit_active_start(const char *job_id,
> BlockDriverState *bs,
> * @mode: Whether to collapse all images in the chain to the target.
> * @backing_mode: How to establish the target's backing chain after
> completion.
> * @zero_target: Whether the target should be explicitly zero-initialized
> + * @target_is_zero: Whether the target already is zero-initialized.
> * @on_source_error: The action to take upon error reading from the source.
> * @on_target_error: The action to take upon error writing to the target.
> * @unmap: Whether to unmap target where source sectors only contain zeroes.
> @@ -159,7 +160,7 @@ void mirror_start(const char *job_id, BlockDriverState
> *bs,
> int creation_flags, int64_t speed,
> uint32_t granularity, int64_t buf_size,
> MirrorSyncMode mode, BlockMirrorBackingMode backing_mode,
> - bool zero_target,
> + bool zero_target, bool target_is_zero,
> BlockdevOnError on_source_error,
> BlockdevOnError on_target_error,
> bool unmap, const char *filter_node_name,
> diff --git a/block/mirror.c b/block/mirror.c
> index 2599b75d092..4dcb50c81ad 100644
> --- a/block/mirror.c
> +++ b/block/mirror.c
> @@ -55,6 +55,8 @@ typedef struct MirrorBlockJob {
> BlockMirrorBackingMode backing_mode;
> /* Whether the target image requires explicit zero-initialization */
> bool zero_target;
> + /* Whether the target should be assumed to be already zero initialized */
> + bool target_is_zero;
> /*
> * To be accesssed with atomics. Written only under the BQL (required by
> the
> * current implementation of mirror_change()).
> @@ -844,12 +846,26 @@ static int coroutine_fn GRAPH_UNLOCKED
> mirror_dirty_init(MirrorBlockJob *s)
> BlockDriverState *target_bs = blk_bs(s->target);
> int ret = -EIO;
> int64_t count;
> + bool punch_holes =
> + target_bs->detect_zeroes == BLOCKDEV_DETECT_ZEROES_OPTIONS_UNMAP &&
> + bdrv_can_write_zeroes_with_unmap(target_bs);
>
> bdrv_graph_co_rdlock();
> bs = s->mirror_top_bs->backing->bs;
> bdrv_graph_co_rdunlock();
>
> - if (s->zero_target) {
> + if (s->zero_target && (!s->target_is_zero || punch_holes)) {
> + /*
> + * Here, we are in FULL mode; our goal is to avoid writing
> + * zeroes if the destination already reads as zero, except
> + * when we are trying to punch holes. This is possible if
> + * zeroing happened externally (s->target_is_zero) or if we
> + * have a fast way to pre-zero the image (the dirty bitmap
> + * will be populated later by the non-zero portions, the same
> + * as for TOP mode). If pre-zeroing is not fast, or we need
> + * to punch holes, then our only recourse is to write the
> + * entire image.
> + */
> if (!bdrv_can_write_zeroes_with_unmap(target_bs)) {
> bdrv_set_dirty_bitmap(s->dirty_bitmap, 0, s->bdev_length);
> return 0;
> @@ -1714,7 +1730,7 @@ static BlockJob *mirror_start_job(
> uint32_t granularity, int64_t buf_size,
> MirrorSyncMode sync_mode,
> BlockMirrorBackingMode backing_mode,
> - bool zero_target,
> + bool zero_target, bool target_is_zero,
> BlockdevOnError on_source_error,
> BlockdevOnError on_target_error,
> bool unmap,
> @@ -1883,6 +1899,7 @@ static BlockJob *mirror_start_job(
> s->sync_mode = sync_mode;
> s->backing_mode = backing_mode;
> s->zero_target = zero_target;
> + s->target_is_zero = target_is_zero;
> qatomic_set(&s->copy_mode, copy_mode);
> s->base = base;
> s->base_overlay = bdrv_find_overlay(bs, base);
> @@ -2011,7 +2028,7 @@ void mirror_start(const char *job_id, BlockDriverState
> *bs,
> int creation_flags, int64_t speed,
> uint32_t granularity, int64_t buf_size,
> MirrorSyncMode mode, BlockMirrorBackingMode backing_mode,
> - bool zero_target,
> + bool zero_target, bool target_is_zero,
> BlockdevOnError on_source_error,
> BlockdevOnError on_target_error,
> bool unmap, const char *filter_node_name,
> @@ -2034,7 +2051,8 @@ void mirror_start(const char *job_id, BlockDriverState
> *bs,
>
> mirror_start_job(job_id, bs, creation_flags, target, replaces,
> speed, granularity, buf_size, mode, backing_mode,
> - zero_target, on_source_error, on_target_error, unmap,
> + zero_target,
> + target_is_zero, on_source_error, on_target_error, unmap,
> NULL, NULL, &mirror_job_driver, base, false,
> filter_node_name, true, copy_mode, false, errp);
> }
> @@ -2062,6 +2080,7 @@ BlockJob *commit_active_start(const char *job_id,
> BlockDriverState *bs,
> job = mirror_start_job(
> job_id, bs, creation_flags, base, NULL, speed, 0, 0,
> MIRROR_SYNC_MODE_TOP, MIRROR_LEAVE_BACKING_CHAIN, false,
> + false,
> on_error, on_error, true, cb, opaque,
> &commit_active_job_driver, base, auto_complete,
> filter_node_name, false, MIRROR_COPY_MODE_BACKGROUND,
> diff --git a/blockdev.c b/blockdev.c
> index 1d1f27cfff6..2e2fed539ee 100644
> --- a/blockdev.c
> +++ b/blockdev.c
> @@ -2798,7 +2798,7 @@ static void blockdev_mirror_common(const char *job_id,
> BlockDriverState *bs,
> const char *replaces,
> enum MirrorSyncMode sync,
> BlockMirrorBackingMode backing_mode,
> - bool zero_target,
> + bool zero_target, bool target_is_zero,
> bool has_speed, int64_t speed,
> bool has_granularity, uint32_t
> granularity,
> bool has_buf_size, int64_t buf_size,
> @@ -2909,11 +2909,10 @@ static void blockdev_mirror_common(const char
> *job_id, BlockDriverState *bs,
> /* pass the node name to replace to mirror start since it's loose
> coupling
> * and will allow to check whether the node still exist at mirror
> completion
> */
> - mirror_start(job_id, bs, target,
> - replaces, job_flags,
> + mirror_start(job_id, bs, target, replaces, job_flags,
> speed, granularity, buf_size, sync, backing_mode,
> zero_target,
> - on_source_error, on_target_error, unmap, filter_node_name,
> - copy_mode, errp);
> + target_is_zero, on_source_error, on_target_error, unmap,
> + filter_node_name, copy_mode, errp);
> }
>
> void qmp_drive_mirror(DriveMirror *arg, Error **errp)
> @@ -2928,6 +2927,7 @@ void qmp_drive_mirror(DriveMirror *arg, Error **errp)
> int64_t size;
> const char *format = arg->format;
> bool zero_target;
> + bool target_is_zero;
> int ret;
>
> bs = qmp_get_root_bs(arg->device, errp);
> @@ -3044,6 +3044,8 @@ void qmp_drive_mirror(DriveMirror *arg, Error **errp)
> zero_target = (arg->sync == MIRROR_SYNC_MODE_FULL &&
> (arg->mode == NEW_IMAGE_MODE_EXISTING ||
> !bdrv_has_zero_init(target_bs)));
> + target_is_zero = (arg->mode != NEW_IMAGE_MODE_EXISTING &&
> + bdrv_has_zero_init(target_bs));
Similarly, if the qmp_drive_mirror supports the target-is-zero parameter, we
would then
be able to determine whether the destination image is fully zero-initialized
when
arg->mode is NEW_IMAGE_MODE_EXISTING.
> bdrv_graph_rdunlock_main_loop();
>
>
> @@ -3055,7 +3057,7 @@ void qmp_drive_mirror(DriveMirror *arg, Error **errp)
>
thanks.