Skip to content

Commit 1835135

Browse files
authored
Merge pull request #509 from xcp-ng/gln/align-spans-to-block-size-rupt
partially_populate_device: align span positions to block size for better performance
2 parents 8b1c034 + 5076ce0 commit 1835135

4 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
@@ -372,6 +372,64 @@ def validate(self, vm: VM, dev: str) -> None:
372372
expected_flags = f'--expected-checksum {self.checksum}' if self.checksum is not None else ''
373373
randstream(vm, f'validate {expected_flags} --position {self.position} --size {self.size} {dev}')
374374

375+
def compute_span_layout(dev_size: int, total_size: int, num_spans: int, block_size: int) -> list[tuple[int, int]]:
376+
"""
377+
Compute the positions and sizes of spans across a device.
378+
379+
Spans are distributed such that the first span starts at 0 and the last
380+
span ends at dev_size. Positions are computed right-to-left: each span's
381+
start is rounded UP to the nearest multiple of align so the span sits
382+
entirely within its slot. Bytes skipped by rounding are carried leftward
383+
to the previous span, which absorbs them in its budget.
384+
385+
When total_size equals dev_size, spans are contiguous and cover the full
386+
device with no gaps. When total_size is less than dev_size, spans are
387+
evenly spread across the device with gaps between them.
388+
389+
Args:
390+
dev_size: Total device size in bytes
391+
total_size: Total number of bytes to write across all spans.
392+
Use dev_size for full device coverage, or a smaller value
393+
to leave gaps between spans.
394+
num_spans: Number of spans to create
395+
block_size: Block size in bytes to align span starts to.
396+
All span starts are rounded up to the nearest multiple of align.
397+
Bytes skipped by the rounding are carried to the previous span.
398+
399+
Returns:
400+
List of (position, size) tuples, one per span.
401+
Spans are guaranteed to:
402+
- Not overlap
403+
- Have first span starting at position 0
404+
- Have last span ending at dev_size
405+
- Sum of span sizes equals total_size
406+
"""
407+
per_span = total_size // num_spans
408+
result: list[tuple[int, int]] = []
409+
# Seed the remainder into the last span so it is distributed via carry
410+
carry = total_size % num_spans
411+
end = dev_size
412+
for i in range(num_spans - 1, -1, -1):
413+
budget = per_span + carry
414+
if i == 0:
415+
# First span always starts at 0; any leftover space before the
416+
# next span becomes a gap (partial coverage only)
417+
start = 0
418+
size = min(budget, end)
419+
else:
420+
# Round start UP: this may shrink the span relative to budget;
421+
# the trimmed bytes are carried left to the previous span
422+
ideal_start = end - budget
423+
start = ((ideal_start + block_size - 1) // block_size) * block_size
424+
size = end - start
425+
carry = budget - size
426+
result.append((start, size))
427+
end = start
428+
result.reverse()
429+
assert sum(s for _, s in result) == total_size
430+
return result
431+
432+
375433
def partially_populate_device(vm: VM, dev_path: str, dev_size: int, num_spans: int = 3, skip_spans: list[int] = []) \
376434
-> list[StreamSpan]:
377435
"""
@@ -405,55 +463,25 @@ def partially_populate_device(vm: VM, dev_path: str, dev_size: int, num_spans: i
405463
skip_spans: List of span indices to skip (no data generated).
406464
Skipped spans still exist in returned list with checksum=None.
407465
(default: [])
466+
Span alignment is controlled by the --write-volume-align pytest option.
408467
409468
Returns:
410469
List of StreamSpan objects representing the generated spans.
411-
Spans are guaranteed to:
412-
- Not overlap
413-
- Have first span at position 0
414-
- Have last span ending at dev_size
415-
- Be evenly distributed across device
416470
"""
417471
logging.info(f"Generate {dev_path} content")
418-
stream_size = min(dev_size, config.write_volume_cap) // num_spans
472+
total_size = min(dev_size, config.write_volume_cap)
419473

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

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

458486
return spans
459487

0 commit comments

Comments
 (0)