Skip to content

Commit cd4e983

Browse files
committed
partially_populate_device: align span positions to block size for better performance
Add an optional `align` parameter (default: 4KiB) to align span positions to a given block size. Only positions are aligned (rounded down); span sizes are reduced if needed to avoid overlapping the next span's position. When config.write_volume_cap is large enough, spans are contiguous and cover the full device with no gaps. Without alignment, write throughput drops from ~700MiB/s to ~200MiB/s on large disks. Signed-off-by: Gaëtan Lehmann <gaetan.lehmann@vates.tech>
1 parent 0a82603 commit cd4e983

5 files changed

Lines changed: 331 additions & 35 deletions

File tree

conftest.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,12 +118,20 @@ def pytest_addoption(parser: pytest.Parser) -> None:
118118
help="Maximum amount of data written to a volume."
119119
" Accepts sizes like '1GiB', '2.5TiB', or symbolic values 'VHD_MAX', 'QCOW2_MAX'."
120120
)
121+
parser.addoption(
122+
"--write-volume-align",
123+
action="store",
124+
default="4KiB",
125+
help="Block size to align span positions to in partially_populate_device."
126+
" Accepts sizes like '512', '4KiB', '1MiB'."
127+
)
121128

122129
def pytest_configure(config: pytest.Config) -> None:
123130
global_config.ignore_ssh_banner = config.getoption('--ignore-ssh-banner')
124131
global_config.ssh_output_max_lines = int(config.getoption('--ssh-output-max-lines'))
125132
global_config.volume_size = parse_size(config.getoption('--volume-size'))
126133
global_config.write_volume_cap = parse_size(config.getoption('--write-volume-cap'))
134+
global_config.write_volume_align = parse_size(config.getoption('--write-volume-align'))
127135

128136
def pytest_generate_tests(metafunc: pytest.Metafunc) -> None:
129137
if "vm_ref" in metafunc.fixturenames:

lib/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
ssh_output_max_lines = 20
55
volume_size = 1 * GiB
66
write_volume_cap = 2 * GiB
7+
write_volume_align = 1
78

89
def sr_device_config(datakey: str, *, required: list[str] = []) -> dict[str, str]:
910
import data # import here to avoid depending on this user file for collecting tests

tests/storage/storage.py

Lines changed: 65 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,66 @@ def validate(self, vm: VM, dev: str) -> None:
369369
expected_flags = f'--expected-checksum {self.checksum}' if self.checksum is not None else ''
370370
randstream(vm, f'validate {expected_flags} --position {self.position} --size {self.size} {dev}')
371371

372+
def compute_span_layout(dev_size: int, total_size: int, num_spans: int, align: int) -> list[tuple[int, int]]:
373+
"""
374+
Compute the positions and sizes of spans across a device.
375+
376+
Spans are distributed such that the first span starts at 0 and the last
377+
span ends at dev_size, with middle spans evenly distributed in between.
378+
Positions are computed by evenly dividing total_size across num_spans,
379+
then rounded down to the nearest multiple of align. Bytes that cannot be
380+
written due to alignment rounding are carried over to subsequent spans,
381+
so that the full device can be covered when total_size equals dev_size.
382+
383+
Args:
384+
dev_size: Total device size in bytes
385+
total_size: Total number of bytes to write across all spans.
386+
Use dev_size for full device coverage, or a smaller value
387+
to leave gaps between spans.
388+
num_spans: Number of spans to create
389+
align: Block size in bytes to align span positions to.
390+
All span positions are rounded down to the nearest multiple of align.
391+
Span sizes are reduced if necessary to avoid overlapping the next
392+
span's position, with unwritten bytes carried forward to later spans.
393+
When total_size equals dev_size, spans are contiguous and cover
394+
the full device with no gaps.
395+
396+
Returns:
397+
List of (position, size) tuples, one per span.
398+
Spans are guaranteed to:
399+
- Not overlap
400+
- Have first span at position 0
401+
- Have last span ending at dev_size
402+
- Be evenly distributed across device
403+
"""
404+
# Positions are computed by evenly dividing total_size, then aligned down.
405+
# Using total_size * i // num_spans avoids losing the remainder that would
406+
# occur if we divided total_size by num_spans first.
407+
positions = [(total_size * i // num_spans // align) * align for i in range(num_spans)]
408+
409+
# Compute sizes with carry: bytes trimmed due to alignment are carried
410+
# forward so later spans can make up the difference
411+
result: list[tuple[int, int]] = []
412+
carry = 0
413+
per_span = total_size // num_spans
414+
for i, position in enumerate(positions):
415+
if i == len(positions) - 1:
416+
size = dev_size - position
417+
else:
418+
budget = per_span + carry
419+
size = min(budget, positions[i + 1] - position)
420+
carry = budget - size
421+
assert position + size <= dev_size, \
422+
f"Span {i} at position={position} size={size} extends past dev_size={dev_size}"
423+
if i > 0:
424+
prev_pos, prev_size = result[-1]
425+
assert position >= prev_pos + prev_size, \
426+
f"Span {i} at position {position} overlaps with previous span ending at {prev_pos + prev_size}"
427+
result.append((position, size))
428+
429+
return result
430+
431+
372432
def partially_populate_device(vm: VM, dev_path: str, dev_size: int, num_spans: int = 3, skip_spans: list[int] = []) \
373433
-> list[StreamSpan]:
374434
"""
@@ -402,55 +462,25 @@ def partially_populate_device(vm: VM, dev_path: str, dev_size: int, num_spans: i
402462
skip_spans: List of span indices to skip (no data generated).
403463
Skipped spans still exist in returned list with checksum=None.
404464
(default: [])
465+
Span alignment is controlled by the --write-volume-align pytest option.
405466
406467
Returns:
407468
List of StreamSpan objects representing the generated spans.
408-
Spans are guaranteed to:
409-
- Not overlap
410-
- Have first span at position 0
411-
- Have last span ending at dev_size
412-
- Be evenly distributed across device
413469
"""
414470
logging.info(f"Generate {dev_path} content")
415-
stream_size = min(dev_size, config.write_volume_cap) // num_spans
471+
total_size = min(dev_size, config.write_volume_cap)
416472

417473
# Validate skip_spans
418474
assert all(0 <= i < num_spans for i in skip_spans), \
419475
f"Invalid span index in skip_spans: must be 0 <= i < {num_spans}"
420476

477+
layout = compute_span_layout(dev_size, total_size, num_spans, config.write_volume_align)
421478
spans: list[StreamSpan] = []
422-
423-
# Calculate positions for regularly distributed spans
424-
# First span always at position 0, last span always ends at dev_size
425-
if num_spans == 1:
426-
positions = [0]
427-
elif num_spans == 2:
428-
positions = [0, dev_size - stream_size]
429-
else:
430-
# For 3+ spans: distribute evenly across available space
431-
# Available space is dev_size - size (last span can't extend past dev_size)
432-
available_space = dev_size - stream_size
433-
positions = [0] # First span always at 0
434-
# Distribute middle spans evenly
435-
for i in range(1, num_spans - 1):
436-
position = (available_space * i) // (num_spans - 1)
437-
positions.append(position)
438-
positions.append(dev_size - stream_size) # Last span always ends at dev_size
439-
440-
# Generate spans with consistency checks
441-
prev_end = -1
442-
for i, position in enumerate(positions):
443-
# Assert no overlap
444-
assert position >= prev_end, f"Span {i} at position {position} overlaps with previous span ending at {prev_end}"
445-
span = StreamSpan(position=position, size=stream_size)
479+
for i, (position, size) in enumerate(layout):
480+
span = StreamSpan(position=position, size=size)
446481
if i not in skip_spans:
447482
span.generate(vm, dev_path, seed=1000 + i)
448483
spans.append(span)
449-
prev_end = position + stream_size
450-
451-
# Final assert: last span must not extend past dev_size
452-
assert spans[-1].position + spans[-1].size <= dev_size, \
453-
f"Last span extends past device: position={spans[-1].position}, size={spans[-1].size}, dev_size={dev_size}"
454484

455485
return spans
456486

tests/unit/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)