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 <ebl...@redhat.com>
> Acked-by: Markus Armbruster <arm...@redhat.com>
> 
> ---
> 
> 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.


Reply via email to