blob: 9f37ffbdcde8b852a1371ed7a3984b6ef308b09f [file]
# Copyright 2026 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Unit tests for storage.block_io."""
import asyncio
import json
import pathlib
import tempfile
import unittest
import zipfile
from typing import Any
from unittest.mock import Mock, patch
import storage
from reporting import metrics
def _expected_metrics(
prefix: str | None,
read_bytes: float,
read_ops: float,
write_bytes: float,
write_ops: float,
flush_bytes: float,
flush_ops: float,
trim_bytes: float,
trim_ops: float,
) -> list[metrics.TestCaseResult]:
label_prefix = f"{prefix}/" if prefix else ""
return [
metrics.TestCaseResult(
label=f"{label_prefix}BlockReadBytesPerSec",
unit=metrics.Unit.bytesPerSecond,
values=(read_bytes,),
doc="Cumulative block read bytes per second of device uptime.",
direction=metrics.Direction.smallerIsBetter,
),
metrics.TestCaseResult(
label=f"{label_prefix}BlockReadOpsPerSec",
unit=metrics.Unit.count,
values=(read_ops,),
doc="Cumulative block read operations per second of device uptime.",
direction=metrics.Direction.smallerIsBetter,
),
metrics.TestCaseResult(
label=f"{label_prefix}BlockWriteBytesPerSec",
unit=metrics.Unit.bytesPerSecond,
values=(write_bytes,),
doc="Cumulative block write bytes per second of device uptime.",
direction=metrics.Direction.smallerIsBetter,
),
metrics.TestCaseResult(
label=f"{label_prefix}BlockWriteOpsPerSec",
unit=metrics.Unit.count,
values=(write_ops,),
doc="Cumulative block write operations per second of device uptime.",
direction=metrics.Direction.smallerIsBetter,
),
metrics.TestCaseResult(
label=f"{label_prefix}BlockFlushBytesPerSec",
unit=metrics.Unit.bytesPerSecond,
values=(flush_bytes,),
doc="Cumulative block flush bytes per second of device uptime.",
direction=metrics.Direction.smallerIsBetter,
),
metrics.TestCaseResult(
label=f"{label_prefix}BlockFlushOpsPerSec",
unit=metrics.Unit.count,
values=(flush_ops,),
doc="Cumulative block flush operations per second of device uptime.",
direction=metrics.Direction.smallerIsBetter,
),
metrics.TestCaseResult(
label=f"{label_prefix}BlockTrimBytesPerSec",
unit=metrics.Unit.bytesPerSecond,
values=(trim_bytes,),
doc="Cumulative block trim bytes per second of device uptime.",
direction=metrics.Direction.smallerIsBetter,
),
metrics.TestCaseResult(
label=f"{label_prefix}BlockTrimOpsPerSec",
unit=metrics.Unit.count,
values=(trim_ops,),
doc="Cumulative block trim operations per second of device uptime.",
direction=metrics.Direction.smallerIsBetter,
),
]
class BlockIoTest(unittest.TestCase):
def test_extract_ftl_metrics(self) -> None:
inspect_entries = [
{
"metadata": {
"name": "ftl",
"timestamp": 100_000_000_000, # 100s
},
"payload": {
"root": {
"ftl": {
"block.read.bytes": 1000 * 4096,
"block.read.count": 500,
"block.write.bytes": 510 * 4096,
"block.write.count": 250,
"block.flush.bytes": 10 * 4096,
"block.flush.count": 20,
"block.trim.bytes": 100 * 4096,
"block.trim.count": 10,
}
}
},
}
]
results = storage.extract_storage_io_metrics(
inspect_entries, label_prefix="FtlTest"
)
self.assertEqual(
results,
_expected_metrics(
prefix="FtlTest",
read_bytes=(1000 * 4096) / 100.0,
read_ops=5.0,
write_bytes=(510 * 4096) / 100.0,
write_ops=2.5,
flush_bytes=(10 * 4096) / 100.0,
flush_ops=0.2,
trim_bytes=(100 * 4096) / 100.0,
trim_ops=0.1,
),
)
def test_extract_sdmmc_metrics(self) -> None:
inspect_entries = [
{
"metadata": {
"name": "sdmmc",
"timestamp": 50_000_000_000, # 50s
},
"payload": {
"root": {
"sdmmc_core": {
"block.read.bytes": 2_000_000,
"block.read.count": 400,
"block.write.bytes": 1_000_000,
"block.write.count": 200,
"block.flush.bytes": 0,
"block.flush.count": 50,
"block.trim.bytes": 500_000,
"block.trim.count": 25,
}
}
},
}
]
results = storage.extract_storage_io_metrics(
inspect_entries, label_prefix="EmmcTest"
)
self.assertEqual(
results,
_expected_metrics(
prefix="EmmcTest",
read_bytes=40_000.0,
read_ops=8.0,
write_bytes=20_000.0,
write_ops=4.0,
flush_bytes=0.0,
flush_ops=1.0,
trim_bytes=10_000.0,
trim_ops=0.5,
),
)
def test_extract_virtio_block_metrics(self) -> None:
inspect_entries = [
{
"metadata": {
"name": "virtio-block",
"timestamp": 25_000_000_000, # 25s
},
"payload": {
"root": {
"virtio_block": {
"block.read.bytes": 750_000,
"block.read.count": 150,
"block.write.bytes": 500_000,
"block.write.count": 100,
"block.flush.bytes": 0,
"block.flush.count": 25,
"block.trim.bytes": 250_000,
"block.trim.count": 50,
}
}
},
}
]
results = storage.extract_storage_io_metrics(
inspect_entries, label_prefix="VirtioTest"
)
self.assertEqual(
results,
_expected_metrics(
prefix="VirtioTest",
read_bytes=30_000.0,
read_ops=6.0,
write_bytes=20_000.0,
write_ops=4.0,
flush_bytes=0.0,
flush_ops=1.0,
trim_bytes=10_000.0,
trim_ops=2.0,
),
)
def test_extract_ufs_metrics_via_capture(self) -> None:
inspect_entries: list[dict[str, Any]] = [
{
"metadata": {
"name": "ufs",
"timestamp": 100_000_000_000,
},
"payload": {
"root": {
"ufs": {
"block.read.bytes": 4_000_000,
"block.read.count": 1000,
"block.write.bytes": 2_000_000,
"block.write.count": 500,
"block.flush.bytes": 0,
"block.flush.count": 100,
"block.trim.bytes": 1_000_000,
"block.trim.count": 50,
}
}
},
},
]
report = storage.capture(inspect_entries=inspect_entries)
self.assertEqual(
report.structured,
_expected_metrics(
prefix=None,
read_bytes=40_000.0,
read_ops=10.0,
write_bytes=20_000.0,
write_ops=5.0,
flush_bytes=0.0,
flush_ops=1.0,
trim_bytes=10_000.0,
trim_ops=0.5,
),
)
def test_load_inspect_entries_from_snapshot_zip_and_publish(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
log_path = pathlib.Path(tmp_dir)
teardown_dir = log_path / "teardown_class"
snapshot_zip_path = log_path / "Snapshot_dut_123.zip"
inspect_payload = [
{
"metadata": {
"name": "sdmmc",
"timestamp": 10_000_000_000, # 10s
},
"payload": {
"root": {
"sdmmc_core": {
"block.read.bytes": 81_920,
"block.read.count": 20,
"block.write.bytes": 40_960,
"block.write.count": 10,
"block.flush.bytes": 0,
"block.flush.count": 5,
"block.trim.bytes": 4096,
"block.trim.count": 1,
}
}
},
}
]
with zipfile.ZipFile(snapshot_zip_path, "w") as zf:
zf.writestr("inspect.json", json.dumps(inspect_payload))
class FakeTest:
def __init__(self) -> None:
self.log_path = str(log_path)
self._teardown_class_artifacts = str(teardown_dir)
self.dut = Mock()
fake_test = FakeTest()
with patch(
"storage.block_io.publish.publish_fuchsiaperf"
) as mock_publish:
published = storage.publish_teardown_storage_io_metrics(
fake_test,
test_suite="fuchsia.storage.io",
runtime_deps_dir="runtime_deps",
)
self.assertEqual(len(published), 8)
expected_json_path = (
teardown_dir / "fuchsia.storage.io-benchmark.fuchsiaperf.json"
)
self.assertTrue(expected_json_path.exists())
mock_publish.assert_called_once_with(
fuchsia_perf_file_paths=[expected_json_path],
expected_metric_names_filename="fuchsia.storage.io.txt",
runtime_deps_dir="runtime_deps",
)
def test_deterministic_selection_across_multiple_entries(self) -> None:
idle_entry = storage.InspectEntry(
moniker="bootstrap/boot-drivers:dev.sdmmc.0",
name="sdmmc",
timestamp_ns=50_000_000_000,
root={
"sdmmc_core": {
"block.read.bytes": 0,
"block.read.count": 0,
"block.write.bytes": 0,
"block.write.count": 0,
"block.flush.bytes": 0,
"block.flush.count": 0,
"block.trim.bytes": 0,
"block.trim.count": 0,
}
},
)
active_entry = storage.InspectEntry(
moniker="bootstrap/boot-drivers:dev.sdmmc.1",
name="sdmmc",
timestamp_ns=50_000_000_000,
root={
"sdmmc_core": {
"block.read.bytes": 2_000_000,
"block.read.count": 400,
"block.write.bytes": 1_000_000,
"block.write.count": 200,
"block.flush.bytes": 0,
"block.flush.count": 50,
"block.trim.bytes": 500_000,
"block.trim.count": 25,
}
},
)
expected = _expected_metrics(
prefix="EmmcTest",
read_bytes=40_000.0,
read_ops=8.0,
write_bytes=20_000.0,
write_ops=4.0,
flush_bytes=0.0,
flush_ops=1.0,
trim_bytes=10_000.0,
trim_ops=0.5,
)
self.assertEqual(
storage.extract_storage_io_metrics(
[idle_entry, active_entry], label_prefix="EmmcTest"
),
expected,
)
self.assertEqual(
storage.extract_storage_io_metrics(
[active_entry, idle_entry], label_prefix="EmmcTest"
),
expected,
)
def test_extract_when_name_is_none(self) -> None:
entry = storage.InspectEntry(
moniker="bootstrap/boot-drivers:nand.fvm",
name=None,
timestamp_ns=50_000_000_000,
root={
"ftl": {
"block.read.bytes": 2_000_000,
"block.read.count": 400,
"block.write.bytes": 1_000_000,
"block.write.count": 200,
"block.flush.bytes": 0,
"block.flush.count": 50,
"block.trim.bytes": 500_000,
"block.trim.count": 25,
}
},
)
self.assertEqual(
storage.extract_storage_io_metrics([entry], label_prefix="FtlTest"),
_expected_metrics(
prefix="FtlTest",
read_bytes=40_000.0,
read_ops=8.0,
write_bytes=20_000.0,
write_ops=4.0,
flush_bytes=0.0,
flush_ops=1.0,
trim_bytes=10_000.0,
trim_ops=0.5,
),
)
def test_extract_storage_io_metrics_with_baseline_entries(self) -> None:
start_entry = storage.InspectEntry(
moniker="bootstrap/boot-drivers:dev.ufs",
name=None,
timestamp_ns=100_000_000_000,
root={
"ufs": {
"block.read.bytes": 500_000_000,
"block.read.count": 20_000,
"block.write.bytes": 200_000_000,
"block.write.count": 10_000,
"block.flush.bytes": 0,
"block.flush.count": 500,
"block.trim.bytes": 10_000_000,
"block.trim.count": 100,
}
},
)
end_entry = storage.InspectEntry(
moniker="bootstrap/boot-drivers:dev.ufs",
name=None,
timestamp_ns=110_000_000_000,
root={
"ufs": {
"block.read.bytes": 500_040_960,
"block.read.count": 20_010,
"block.write.bytes": 200_081_920,
"block.write.count": 10_020,
"block.flush.bytes": 0,
"block.flush.count": 505,
"block.trim.bytes": 10_010_000,
"block.trim.count": 102,
}
},
)
self.assertEqual(
storage.extract_storage_io_metrics(
[end_entry],
label_prefix="Storage/System",
baseline_entries=[start_entry],
),
_expected_metrics(
prefix="Storage/System",
read_bytes=4096.0,
read_ops=1.0,
write_bytes=8192.0,
write_ops=2.0,
flush_bytes=0.0,
flush_ops=0.5,
trim_bytes=1000.0,
trim_ops=0.2,
),
)
def test_diff_inspect_entries_handles_zero_delta_with_idle_secondary_driver(
self,
) -> None:
idle_wifi_sdmmc = storage.InspectEntry(
moniker="bootstrap/boot-drivers:wifi-sdio",
name="sdmmc",
timestamp_ns=100_000_000_000,
root={
"sdmmc_core": {
"block.read.bytes": 0,
"block.read.count": 0,
"block.write.bytes": 0,
"block.write.count": 0,
"block.flush.bytes": 0,
"block.flush.count": 0,
"block.trim.bytes": 0,
"block.trim.count": 0,
}
},
)
active_start = storage.InspectEntry(
moniker="bootstrap/boot-drivers:emmc",
name="sdmmc",
timestamp_ns=100_000_000_000,
root={
"sdmmc_core": {
"block.read.bytes": 500_000_000,
"block.read.count": 20_000,
"block.write.bytes": 200_000_000,
"block.write.count": 10_000,
"block.flush.bytes": 0,
"block.flush.count": 500,
"block.trim.bytes": 0,
"block.trim.count": 0,
}
},
)
active_end = storage.InspectEntry(
moniker="bootstrap/boot-drivers:emmc",
name="sdmmc",
timestamp_ns=130_000_000_000,
root=active_start.root,
)
self.assertEqual(
storage.extract_storage_io_metrics(
[idle_wifi_sdmmc, active_end],
label_prefix="Storage/System",
baseline_entries=[idle_wifi_sdmmc, active_start],
),
_expected_metrics(
prefix="Storage/System",
read_bytes=0.0,
read_ops=0.0,
write_bytes=0.0,
write_ops=0.0,
flush_bytes=0.0,
flush_ops=0.0,
trim_bytes=0.0,
trim_ops=0.0,
),
)
def test_storage_io_metrics_processor_structured_and_freeform(self) -> None:
start_entry = storage.InspectEntry(
moniker="bootstrap/boot-drivers:dev.ufs",
name="ufs",
timestamp_ns=100_000_000_000,
root={
"ufs": {
"block.read.bytes": 500_000_000,
"block.read.count": 20_000,
"block.write.bytes": 200_000_000,
"block.write.count": 10_000,
"block.flush.bytes": 0,
"block.flush.count": 500,
"block.trim.bytes": 10_000_000,
"block.trim.count": 100,
}
},
)
end_entry = storage.InspectEntry(
moniker="bootstrap/boot-drivers:dev.ufs",
name="ufs",
timestamp_ns=110_000_000_000,
root={
"ufs": {
"block.read.bytes": 500_040_960,
"block.read.count": 20_010,
"block.write.bytes": 200_081_920,
"block.write.count": 10_020,
"block.flush.bytes": 0,
"block.flush.count": 505,
"block.trim.bytes": 10_010_000,
"block.trim.count": 102,
}
},
)
processor = storage.StorageIoMetricsProcessor(
start_entries=[start_entry],
end_entries=[end_entry],
)
self.assertEqual(processor.event_patterns, set())
self.assertEqual(
processor.process_metrics(),
[
metrics.TestCaseResult(
label="Storage/System/BlockWriteOpsPerSec",
unit=metrics.Unit.count,
values=(2.0,),
doc="Block write operations per second of interval.",
direction=metrics.Direction.smallerIsBetter,
),
metrics.TestCaseResult(
label="Storage/System/BlockFlushOpsPerSec",
unit=metrics.Unit.count,
values=(0.5,),
doc="Block flush operations per second of interval.",
direction=metrics.Direction.smallerIsBetter,
),
],
)
self.assertEqual(
processor.process_freeform_metrics(),
(
"storage_io",
{
"read": {
"bytes_per_sec": 4096.0,
"ops_per_sec": 1.0,
},
"write": {
"bytes_per_sec": 8192.0,
"ops_per_sec": 2.0,
},
"flush": {
"bytes_per_sec": 0.0,
"ops_per_sec": 0.5,
},
"trim": {
"bytes_per_sec": 1000.0,
"ops_per_sec": 0.2,
},
},
),
)
def test_storage_io_metrics_processor_context_manager_with_dut(
self,
) -> None:
start_entry = storage.InspectEntry(
moniker="bootstrap/boot-drivers:dev.ufs",
name="ufs",
timestamp_ns=100_000_000_000,
root={
"ufs": {
"block.read.bytes": 1_000_000,
"block.read.count": 100,
"block.write.bytes": 2_000_000,
"block.write.count": 200,
"block.flush.bytes": 0,
"block.flush.count": 50,
"block.trim.bytes": 0,
"block.trim.count": 0,
}
},
)
end_entry = storage.InspectEntry(
moniker="bootstrap/boot-drivers:dev.ufs",
name="ufs",
timestamp_ns=105_000_000_000,
root={
"ufs": {
"block.read.bytes": 1_020_000,
"block.read.count": 105,
"block.write.bytes": 2_050_000,
"block.write.count": 215,
"block.flush.bytes": 0,
"block.flush.count": 55,
"block.trim.bytes": 0,
"block.trim.count": 0,
}
},
)
dut = Mock()
with patch(
"storage.block_io.load_inspect_entries",
side_effect=[[start_entry], [end_entry]],
) as mock_load:
with storage.StorageIoMetricsProcessor(dut) as processor:
pass
self.assertEqual(mock_load.call_count, 2)
self.assertEqual(
[m.values for m in processor.process_metrics()],
[(3.0,), (1.0,)],
)
def test_storage_io_metrics_processor_empty_end_entries(self) -> None:
start_entry = storage.InspectEntry(
moniker="bootstrap/boot-drivers:dev.ufs",
name="ufs",
timestamp_ns=100_000_000_000,
root={
"ufs": {
"block.read.bytes": 1_000_000,
"block.read.count": 100,
"block.write.bytes": 2_000_000,
"block.write.count": 200,
"block.flush.bytes": 0,
"block.flush.count": 50,
"block.trim.bytes": 0,
"block.trim.count": 0,
}
},
)
processor = storage.StorageIoMetricsProcessor(
start_entries=[start_entry], end_entries=[]
)
self.assertEqual(processor.process_metrics(), [])
self.assertEqual(
processor.process_freeform_metrics(), ("storage_io", {})
)
def test_storage_io_metrics_processor_async_context_manager(
self,
) -> None:
async def run_test() -> storage.StorageIoMetricsProcessor:
async with storage.StorageIoMetricsProcessor(dut) as p:
return p
start_entry = storage.InspectEntry(
moniker="bootstrap/boot-drivers:dev.ufs",
name="ufs",
timestamp_ns=100_000_000_000,
root={
"ufs": {
"block.read.bytes": 1_000_000,
"block.read.count": 100,
"block.write.bytes": 2_000_000,
"block.write.count": 200,
"block.flush.bytes": 0,
"block.flush.count": 50,
"block.trim.bytes": 0,
"block.trim.count": 0,
}
},
)
end_entry = storage.InspectEntry(
moniker="bootstrap/boot-drivers:dev.ufs",
name="ufs",
timestamp_ns=105_000_000_000,
root={
"ufs": {
"block.read.bytes": 1_020_000,
"block.read.count": 105,
"block.write.bytes": 2_050_000,
"block.write.count": 215,
"block.flush.bytes": 0,
"block.flush.count": 55,
"block.trim.bytes": 0,
"block.trim.count": 0,
}
},
)
dut = Mock()
with patch(
"storage.block_io.load_inspect_entries",
side_effect=[[start_entry], [end_entry]],
) as mock_load:
processor = asyncio.run(run_test())
self.assertEqual(mock_load.call_count, 2)
self.assertEqual(
[m.values for m in processor.process_metrics()],
[(3.0,), (1.0,)],
)
if __name__ == "__main__":
unittest.main()