If you split a BytesMut object into two, the two objects will still share the underlying reference-counted buffer. Here's an attempt at a visualization, containing a few implementation details. Before splitting:
Underlying buffer, ref count 1
┌────────────────────────────────┐
│0123456789ABCDEFGHIJ │
└▲───────────────────────────────┘
│
│ first
│ ┌────────────┐
├──┤ptr │
│ │len: 20 │
│ │cap: 32 │
└──┤data │
└────────────┘
After calling let second = first.split_off(10), we will get
Underlying buffer, ref count 2
┌────────────────────────────────┐
│0123456789ABCDEFGHIJ │
└▲─────────▲─────────────────────┘
│ │
│ └──────────┐
│ first │ second
│ ┌────────────┐ │ ┌────────────┐
├──┤ptr │ └─────┤ptr │
│ │len: 10 │ │len: 10 │
│ │cap: 10 │ │cap: 22 │
├──┤data │ ┌─────┤data │
│ └────────────┘ │ └────────────┘
│ │
└────────────────────┘
Once we drop first, we will have
Underlying buffer, ref count 1
┌────────────────────────────────┐
│0123456789ABCDEFGHIJ │
└▲─────────▲─────────────────────┘
│ │
│ │
│ │ second
│ │ ┌────────────┐
│ └────────────────┤ptr │
│ │len: 10 │
│ │cap: 22 │
└──────────────────────────┤data │
└────────────┘
If you now call second.reserve(10), or call an operation that implicitly reserves, like writing more than fits in the current capacity, the BytesMut implementation can detect that second actually owns its underlying buffer, since the reference count is one. The implementation then may be able to reuse spare capacity in the buffer by moving the existing buffer contents to the beginning, so after second.reserve(20), the result could look like this:
Underlying buffer, ref count 1
┌────────────────────────────────┐
│ABCDEFGHIJ │
└▲───────────────────────────────┘
│
│ second
│ ┌────────────┐
├──┤ptr │
│ │len: 10 │
│ │cap: 32 │
└──┤data │
└────────────┘
However, the conditions for this optimization to be applied are not guaranteed. The documentation for reserve() states (emphasis mine)
Before allocating new buffer space, the function will attempt to reclaim space in the existing buffer. If the current handle references a view into a larger original buffer, and all other handles referencing part of the same original buffer have been dropped, then the current view can be copied/shifted to the front of the buffer and the handle can take ownership of the full buffer, provided that the full buffer is large enough to fit the requested additional capacity.
This optimization will only happen if shifting the data from the current view to the front of the buffer is not too expensive in terms of the (amortized) time required. The precise condition is subject to change; as of now, the length of the data being shifted needs to be at least as large as the distance that it’s shifted by. If the current view is empty and the original buffer is large enough to fit the requested additional capacity, then reallocations will never happen.
In summary, this optimization is only guaranteed if the reference count is one and the view is empty. This is the case in your example, so your code is guaranteed to reuse the buffer.