> Task size was capped to block_copy_chunk_size() regardless of method,
> which sizes a task by the copy buffer. A write-zeroes task carries no
> buffer, so that cap only splits one long known-zero run into a crowd of
> small COPY_WRITE_ZEROES tasks, each with its own request against the
> target.
> 
> block_copy_task_create() already decides from zero_bitmap how far a task
> may run, so add block_copy_widen_zero_area() to that decision: for a
> write-zeroes task, re-search the dirty area under BDRV_REQUEST_MAX_BYTES
> instead of the buffer chunk size, and let the existing clamp cut it back
> to where the zero run ends. That bound is INT_MAX rounded down to a
> sector, so it is aligned down at the use site, nothing bounding
> cluster_size. Widening is safe: an overlapping caller waits on the
> task's BlockReq via reqlist_wait_one() rather than observing it
> mid-flight.
> 
> A request that large is only cheap where the target zeroes by metadata.
> BDRV_REQ_NO_FALLBACK in supported_zero_flags rules out the targets which
> cannot, qcow2 v2 and iscsi among them, but it is optimistic for the rest:
> file-posix advertises it at open and only learns from a failing
> fallocate. So the first widened request also asks for it, which costs
> nothing when the target obliges and fails without writing when it does
> not. The run then keeps every write-zeroes request at the buffer chunk
> size, and the range that found out is written in those chunks.
> 
> Signed-off-by: Denis V. Lunev <[email protected]>
> CC: Vladimir Sementsov-Ogievskiy <[email protected]>
> CC: John Snow <[email protected]>
> CC: Andrey Drobyshev <[email protected]>
>
> diff --git a/block/block-copy.c b/block/block-copy.c
> index c0a33599384..16f3d048195 100644
> --- a/block/block-copy.c
> +++ b/block/block-copy.c
> @@ -38,6 +38,12 @@
>  #define BLOCK_COPY_SLICE_TIME 100000000ULL /* ns */
>  #define BLOCK_COPY_CLUSTER_SIZE_DEFAULT (1 << 16)
>  
> +typedef enum {
> +    ZERO_WIDEN_UNPROBED,
> +    ZERO_WIDEN_ON,
> +    ZERO_WIDEN_OFF,
> +} BlockCopyZeroWiden;
> +
>  typedef enum {
>      COPY_READ_WRITE_CLUSTER,
>      COPY_READ_WRITE,
> @@ -159,6 +165,8 @@ typedef struct BlockCopyState {
>      bool skip_unallocated; /* atomic */
>      /* State fields that use a thread-safe API */
>      BdrvDirtyBitmap *copy_bitmap;
> +    /* Whether the target zeroes by metadata; see block_copy_write_zeroes(). 
> */
> +    BlockCopyZeroWiden zero_widen; /* atomic */
>      /* Clusters reading as zero; allocated on demand, frozen once valid. */
>      HBitmap *zero_bitmap;
>      /* Published only after the scan, with skip_unallocated already false. */
> @@ -187,6 +195,34 @@ static int64_t block_copy_chunk_size(BlockCopyState *s)
>      }
>  }
>  
> +/*
> + * A write-zeroes task carries no buffer, so it may cover far more than
> + * block_copy_chunk_size(). Return how far it may run; the caller clamps it
> + * to where the zero run ends.
> + */
> +static int64_t block_copy_widen_zero_area(BlockCopyState *s,
> +                                          BlockCopyCallState *call_state,
> +                                          int64_t offset, int64_t search_end,
> +                                          int64_t bytes)
> +{
> +    int64_t aligned = QEMU_ALIGN_DOWN(BDRV_REQUEST_MAX_BYTES,
> +                                      s->cluster_size);
> +    int64_t zero_chunk = MIN_NON_ZERO(MAX(aligned, s->cluster_size),
> +                                      call_state->max_chunk);
> +    int64_t wide_offset, wide_bytes;
> +
> +    if (!bdrv_dirty_bitmap_next_dirty_area(s->copy_bitmap, offset, 
> search_end,
> +                                           zero_chunk, &wide_offset,
> +                                           &wide_bytes)) {
> +        return bytes;
> +    }
> +
> +    /* @offset is dirty, so the search cannot have moved past it. */
> +    assert(wide_offset == offset);
> +
> +    return wide_bytes;
> +}
> +
>  /*
>   * Search for the first dirty area in offset/bytes range and create task at
>   * the beginning of it.
> @@ -198,6 +234,7 @@ block_copy_task_create(BlockCopyState *s, 
> BlockCopyCallState *call_state,
>      BlockCopyTask *task;
>      BlockCopyMethod method;
>      int64_t max_chunk;
> +    int64_t search_end = offset + bytes;
>  
>      QEMU_LOCK_GUARD(&s->lock);
>      max_chunk = MIN_NON_ZERO(block_copy_chunk_size(s), 
> call_state->max_chunk);
> @@ -219,6 +256,10 @@ block_copy_task_create(BlockCopyState *s, 
> BlockCopyCallState *call_state,
>  
>          if (hbitmap_get(s->zero_bitmap, offset)) {
>              method = COPY_WRITE_ZEROES;
> +            if (qatomic_read(&s->zero_widen) != ZERO_WIDEN_OFF) {

There's a potential race here.  Imagine:

  1) task A is created and widened here;
  2) task B is created and widened here;
  3) in block_copy_write_zeroes() we get -ENOTSUP on task A and
     set zero_widen=OFF.
  4) in block_copy_write_zeroes() OFF != UNPROBED, we skip the probing
     and go via slow path.

The race is minor, but still.  Maybe we don't need tristate enum here
and might just make zero_widen a bool, so every fast-path request
carries NO_FALLBACK (not just the 1st request)?

> +                bytes = block_copy_widen_zero_area(s, call_state, offset,
> +                                                   search_end, bytes);
> +            }
>              boundary = hbitmap_next_zero(s->zero_bitmap, offset, bytes);
>          } else {
>              boundary = hbitmap_next_dirty(s->zero_bitmap, offset, bytes);
> @@ -465,6 +506,8 @@ BlockCopyState *block_copy_state_new(BdrvChild *source, 
> BdrvChild *target,
>          .max_transfer = QEMU_ALIGN_DOWN(
>                                      block_copy_max_transfer(source, target),
>                                      cluster_size),
> +        .zero_widen = target->bs->supported_zero_flags & 
> BDRV_REQ_NO_FALLBACK ?
> +                      ZERO_WIDEN_UNPROBED : ZERO_WIDEN_OFF,
>      };
>  
>      s->discard_source = discard_source;
> @@ -521,6 +564,54 @@ static coroutine_fn int block_copy_task_run(AioTaskPool 
> *pool,
>      return 0;
>  }
>  
> +/*
> + * Widening a write-zeroes request only pays off where the target zeroes by
> + * metadata. Ask the first one to fail instead of falling back to writing the
> + * zeroes out: it then costs nothing, and a target which would have written
> + * them keeps its requests small for the rest of the run.
> + */
> +static int coroutine_fn GRAPH_RDLOCK
> +block_copy_write_zeroes(BlockCopyState *s, int64_t offset, int64_t bytes,
> +                        bool *error_is_read)
> +{
> +    BdrvRequestFlags flags = s->write_flags & ~BDRV_REQ_WRITE_COMPRESSED;
> +    int64_t chunk = bytes;
> +    int ret = 0;
> +
> +    if (qatomic_read(&s->zero_widen) == ZERO_WIDEN_UNPROBED) {
> +        ret = bdrv_co_pwrite_zeroes(s->target, offset, bytes,
> +                                    flags | BDRV_REQ_NO_FALLBACK);
> +        if (ret != -ENOTSUP) {

And what if it's -EIO?  Why not 'if (ret == 0)'?

> +            qatomic_set(&s->zero_widen, ZERO_WIDEN_ON);
> +            goto out;
> +        }
> +
> +        /* Nothing has been written, so the whole range is still to do. */
> +        qatomic_set(&s->zero_widen, ZERO_WIDEN_OFF);
> +        chunk = block_copy_chunk_size(s);

block_copy_chunk_size() own doc says "Called with lock held", but we
aren't doing so here.  Should we?

Andrey

-- 
Andrey Drobyshev <[email protected]>

Reply via email to