Skip to content

Commit 7d3fc3e

Browse files
committed
partially_populate_device: align span positions to block size for better performance
Add an optional `align` parameter (default: 1) 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 ce36044 commit 7d3fc3e

5 files changed

Lines changed: 329 additions & 35 deletions

File tree

conftest.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,13 @@ def pytest_addoption(parser: pytest.Parser) -> None:
129129
help="Maximum amount of data written to a volume."
130130
" Accepts sizes like '1GiB', '2.5TiB', or symbolic values 'VHD_MAX', 'QCOW2_MAX'."
131131
)
132+
parser.addoption(
133+
"--write-volume-align",
134+
action="store",
135+
default="1",
136+
help="Block size to align span positions to when writing in volumes."
137+
" Accepts sizes like '512', '4KiB', '1MiB'. A value of 1 is equivalent to no alignment."
138+
)
132139

133140
def pytest_configure(config: pytest.Config) -> None:
134141
global_config.ignore_ssh_banner = config.getoption('--ignore-ssh-banner')
@@ -141,6 +148,9 @@ def pytest_configure(config: pytest.Config) -> None:
141148
write_volume_cap = config.getoption('--write-volume-cap')
142149
assert write_volume_cap is not None
143150
global_config.write_volume_cap = parse_size(write_volume_cap)
151+
write_volume_align = config.getoption('--write-volume-align')
152+
assert write_volume_align is not None
153+
global_config.write_volume_align = parse_size(write_volume_align)
144154

145155
def pytest_generate_tests(metafunc: pytest.Metafunc) -> None:
146156
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: 63 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,64 @@ 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, block_size: 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. Positions are computed right-to-left: each span's
378+
start is rounded UP to the nearest multiple of align so the span sits
379+
entirely within its slot. Bytes skipped by rounding are carried leftward
380+
to the previous span, which absorbs them in its budget.
381+
382+
When total_size equals dev_size, spans are contiguous and cover the full
383+
device with no gaps. When total_size is less than dev_size, spans are
384+
evenly spread across the device with gaps between them.
385+
386+
Args:
387+
dev_size: Total device size in bytes
388+
total_size: Total number of bytes to write across all spans.
389+
Use dev_size for full device coverage, or a smaller value
390+
to leave gaps between spans.
391+
num_spans: Number of spans to create
392+
block_size: Block size in bytes to align span starts to.
393+
All span starts are rounded up to the nearest multiple of align.
394+
Bytes skipped by the rounding are carried to the previous span.
395+
396+
Returns:
397+
List of (position, size) tuples, one per span.
398+
Spans are guaranteed to:
399+
- Not overlap
400+
- Have first span starting at position 0
401+
- Have last span ending at dev_size
402+
- Sum of span sizes equals total_size
403+
"""
404+
per_span = total_size // num_spans
405+
result: list[tuple[int, int]] = []
406+
# Seed the remainder into the last span so it is distributed via carry
407+
carry = total_size % num_spans
408+
end = dev_size
409+
for i in range(num_spans - 1, -1, -1):
410+
budget = per_span + carry
411+
if i == 0:
412+
# First span always starts at 0; any leftover space before the
413+
# next span becomes a gap (partial coverage only)
414+
start = 0
415+
size = min(budget, end)
416+
else:
417+
# Round start UP: this may shrink the span relative to budget;
418+
# the trimmed bytes are carried left to the previous span
419+
ideal_start = end - budget
420+
start = ((ideal_start + block_size - 1) // block_size) * block_size
421+
size = end - start
422+
carry = budget - size
423+
result.append((start, size))
424+
end = start
425+
result.reverse()
426+
assert sum(s for _, s in result) == total_size
427+
return result
428+
429+
372430
def partially_populate_device(vm: VM, dev_path: str, dev_size: int, num_spans: int = 3, skip_spans: list[int] = []) \
373431
-> list[StreamSpan]:
374432
"""
@@ -402,55 +460,25 @@ def partially_populate_device(vm: VM, dev_path: str, dev_size: int, num_spans: i
402460
skip_spans: List of span indices to skip (no data generated).
403461
Skipped spans still exist in returned list with checksum=None.
404462
(default: [])
463+
Span alignment is controlled by the --write-volume-align pytest option.
405464
406465
Returns:
407466
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
413467
"""
414468
logging.info(f"Generate {dev_path} content")
415-
stream_size = min(dev_size, config.write_volume_cap) // num_spans
469+
total_size = min(dev_size, config.write_volume_cap)
416470

417471
# Validate skip_spans
418472
assert all(0 <= i < num_spans for i in skip_spans), \
419473
f"Invalid span index in skip_spans: must be 0 <= i < {num_spans}"
420474

475+
layout = compute_span_layout(dev_size, total_size, num_spans, config.write_volume_align)
421476
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)
477+
for i, (position, size) in enumerate(layout):
478+
span = StreamSpan(position=position, size=size)
446479
if i not in skip_spans:
447480
span.generate(vm, dev_path, seed=1000 + i)
448481
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}"
454482

455483
return spans
456484

tests/unit/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)