This commit is contained in:
2026-08-18 09:20:44 +02:00
commit b826cd721a
522 changed files with 93730 additions and 0 deletions
+853
View File
@@ -0,0 +1,853 @@
#!/usr/bin/env python3
"""Independent multi-block directory and xattr oracle for B19a."""
from __future__ import annotations
import argparse
from concurrent.futures import ThreadPoolExecutor
import hashlib
import json
import os
from pathlib import Path
import statistics
import struct
import threading
import time
SUPER = 1024
MAGIC = 0xE0F5E1E2
CRC32C_POLY = 0x82F63B78
class Reject(RuntimeError):
def __init__(self, errno_name: str, point: str):
super().__init__(f"{errno_name} at {point}")
self.errno_name = errno_name
self.point = point
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def crc32c(data: bytes, seed: int = 0xFFFFFFFF) -> int:
value = seed
for byte in data:
value ^= byte
for _ in range(8):
value = (value >> 1) ^ (CRC32C_POLY if value & 1 else 0)
return value & 0xFFFFFFFF
def load_spec(path: Path) -> dict:
spec = json.loads(path.read_text(encoding="ascii"))
if (
spec.get("schema") != 1
or spec.get("batch") != "B19a"
or spec.get("candidate") != "P15-022"
):
raise SystemExit("invalid B19a oracle spec identity")
return spec
class ReadStats:
def __init__(self) -> None:
self.lock = threading.Lock()
self.calls = 0
self.bytes = 0
self.block_reads = 0
def add(self, offset: int, length: int, block_size: int) -> None:
with self.lock:
self.calls += 1
self.bytes += length
if length:
self.block_reads += (
(offset + length - 1) // block_size - offset // block_size + 1
)
def reset(self) -> None:
with self.lock:
self.calls = 0
self.bytes = 0
self.block_reads = 0
def snapshot(self) -> dict[str, int]:
with self.lock:
return {
"bytes": self.bytes,
"calls": self.calls,
"provider_block_reads": self.block_reads,
}
class Reader:
def __init__(self, path: Path):
self.path = path
self.fd = os.open(path, os.O_RDONLY)
self.size = os.fstat(self.fd).st_size
self.stats = ReadStats()
header = self.read(SUPER, 144, "super")
if struct.unpack_from("<I", header)[0] != MAGIC:
raise Reject("EINTEGRITY", "super.magic")
self.feature_compat = struct.unpack_from("<I", header, 8)[0]
self.block_bits = header[12]
if self.block_bits < 9 or self.block_bits > 16:
raise Reject("EINTEGRITY", "super.block-size")
self.block_size = 1 << self.block_bits
self.root_nid = struct.unpack_from("<H", header, 14)[0]
self.blocks = struct.unpack_from("<I", header, 36)[0]
self.limit = self.blocks << self.block_bits
self.meta_blkaddr = struct.unpack_from("<I", header, 40)[0]
self.xattr_blkaddr = struct.unpack_from("<I", header, 44)[0]
if self.limit > self.size:
raise Reject("EINTEGRITY", "super.bounds")
if self.feature_compat & 1:
expected = struct.unpack_from("<I", header, 4)[0]
raw = bytearray(self.read(SUPER, self.block_size - SUPER, "super.checksum"))
raw[4:8] = bytes(4)
if crc32c(bytes(raw)) != expected:
raise Reject("EINTEGRITY", "super.checksum")
self.stats.reset()
def close(self) -> None:
os.close(self.fd)
def __enter__(self) -> "Reader":
return self
def __exit__(self, *_: object) -> None:
self.close()
def read(self, offset: int, length: int, point: str) -> bytes:
limit = self.limit if hasattr(self, "limit") else self.size
if offset < 0 or length < 0 or offset > limit or length > limit - offset:
raise Reject("EINTEGRITY", f"{point}.bounds")
data = os.pread(self.fd, length, offset)
if len(data) != length:
raise Reject("EINTEGRITY", f"{point}.short")
block_size = self.block_size if hasattr(self, "block_size") else 4096
self.stats.add(offset, length, block_size)
return data
def inode(self, nid: int) -> dict[str, int]:
offset = (self.meta_blkaddr << self.block_bits) + (nid << 5)
raw = self.read(offset, 64, "inode")
inode_format, xattr_count = struct.unpack_from("<HH", raw)
inode_size = 64 if inode_format & 1 else 32
xattr_size = 0 if xattr_count == 0 else 12 + 4 * (xattr_count - 1)
size = struct.unpack_from("<Q" if inode_size == 64 else "<I", raw, 8)[0]
return {
"nid": nid,
"offset": offset,
"inode_size": inode_size,
"xattr_size": xattr_size,
"layout": (inode_format >> 1) & 7,
"size": size,
"start_block": struct.unpack_from("<I", raw, 16)[0],
}
def inode_data(self, inode: dict[str, int], logical: int, length: int) -> bytes:
if logical > inode["size"] or length > inode["size"] - logical:
raise Reject("EINTEGRITY", "inode.data-range")
if inode["layout"] == 0:
physical = (inode["start_block"] << self.block_bits) + logical
elif inode["layout"] == 2:
tail_start = ((inode["size"] + self.block_size - 1) // self.block_size - 1) * self.block_size
if logical < tail_start:
if logical + length > tail_start:
raise Reject("EINTEGRITY", "inode.inline-crossing")
physical = (inode["start_block"] << self.block_bits) + logical
else:
physical = (
inode["offset"]
+ inode["inode_size"]
+ inode["xattr_size"]
+ logical
- tail_start
)
else:
raise Reject("EOPNOTSUPP", "inode.layout")
return self.read(physical, length, "inode.data")
@staticmethod
def parse_dirblock(data: bytes, block_index: int) -> list[dict[str, object]]:
if len(data) < 12:
raise Reject("EINTEGRITY", "directory.header")
first_name = struct.unpack_from("<H", data, 8)[0]
if first_name < 12 or first_name >= len(data) or first_name % 12:
raise Reject("EINTEGRITY", "directory.name-offset")
count = first_name // 12
entries = []
previous_offset = 0
previous_name: bytes | None = None
for index in range(count):
offset = index * 12
nid, start, file_type = struct.unpack_from("<QHB", data, offset)
end = (
struct.unpack_from("<H", data, offset + 20)[0]
if index + 1 < count
else len(data)
)
if (
start < first_name
or start >= len(data)
or (index == 0 and start != first_name)
or (index != 0 and start <= previous_offset)
or end <= start
or end > len(data)
):
raise Reject("EINTEGRITY", "directory.name-bounds")
span = data[start:end]
if index + 1 < count:
if b"\0" in span:
raise Reject("EINTEGRITY", "directory.name-nul")
name = span
else:
name = span.split(b"\0", 1)[0]
if not name or len(name) > 255 or b"/" in name:
raise Reject("EINTEGRITY", "directory.name")
if previous_name is not None and previous_name >= name:
raise Reject("EINTEGRITY", "directory.order")
entries.append(
{
"block": block_index,
"entry": index,
"file_type": file_type,
"name": name,
"nid": nid,
}
)
previous_offset = start
previous_name = name
return entries
@staticmethod
def validate_boundary(
left: list[dict[str, object]], right: list[dict[str, object]]
) -> None:
if left[-1]["name"] >= right[0]["name"]:
raise Reject("EINTEGRITY", "directory.boundary-order")
@staticmethod
def resolve_entries(entries: list[dict[str, object]], name: bytes) -> int:
matches = [int(entry["nid"]) for entry in entries if entry["name"] == name]
if not matches:
raise Reject("ENOENT", "path.missing")
if len(matches) != 1:
raise Reject("EINTEGRITY", "path.duplicate")
return matches[0]
def directory_entries(self, inode: dict[str, int]) -> list[dict[str, object]]:
entries = []
previous_name: bytes | None = None
logical = 0
block_index = 0
while logical < inode["size"]:
length = min(self.block_size, inode["size"] - logical)
block_entries = self.parse_dirblock(
self.inode_data(inode, logical, length), block_index
)
if entries:
self.validate_boundary(entries, block_entries)
entries.extend(block_entries)
previous_name = block_entries[-1]["name"]
logical += length
block_index += 1
return entries
def resolve(self, path: str) -> dict[str, int]:
inode = self.inode(self.root_nid)
for component in path.strip("/").encode("ascii").split(b"/"):
if not component:
continue
inode = self.inode(
self.resolve_entries(self.directory_entries(inode), component)
)
return inode
def body_offset(self, inode: dict[str, int]) -> int:
return inode["offset"] + inode["inode_size"]
@staticmethod
def parse_entry(raw: bytes, offset: int, limit: int, point: str) -> tuple[dict, int]:
if offset > limit or 4 > limit - offset:
raise Reject("EINTEGRITY", f"{point}.header")
name_length, name_index, value_length = struct.unpack_from("<BBH", raw, offset)
total = (4 + name_length + value_length + 3) & ~3
if total > limit - offset:
raise Reject("EINTEGRITY", f"{point}.bounds")
name_start = offset + 4
name_end = name_start + name_length
value_end = name_end + value_length
name = raw[name_start:name_end]
if b"\0" in name:
raise Reject("EINTEGRITY", f"{point}.name-nul")
return (
{
"index": name_index,
"name": name,
"value": raw[name_end:value_end],
},
total,
)
def shared_entry(self, shared_id: int) -> dict:
offset = (self.xattr_blkaddr << self.block_bits) + shared_id * 4
header = self.read(offset, 4, "shared.header")
name_length, _, value_length = struct.unpack("<BBH", header)
total = (4 + name_length + value_length + 3) & ~3
raw = self.read(offset, total, "shared.entry")
entry, parsed = self.parse_entry(raw, 0, len(raw), "shared")
if parsed != total:
raise Reject("EINTEGRITY", "shared.size")
return entry
def load_body(self, inode: dict[str, int], validate_shared: bool) -> dict:
size = inode["xattr_size"]
if size < 12:
raise Reject("EINTEGRITY", "ibody.header")
if size == 12:
raise Reject("EOPNOTSUPP", "ibody.header-only")
raw = self.read(self.body_offset(inode), size, "ibody")
shared_count = raw[4]
header_size = 12 + shared_count * 4
if header_size > size:
raise Reject("EINTEGRITY", "ibody.shared-count")
shared_ids = [
struct.unpack_from("<I", raw, 12 + index * 4)[0]
for index in range(shared_count)
]
inline = []
cursor = header_size
while cursor < size:
entry, total = self.parse_entry(raw, cursor, size, "inline")
inline.append(entry)
cursor += total
if validate_shared:
for shared_id in shared_ids:
self.shared_entry(shared_id)
return {
"header_size": header_size,
"inline": inline,
"raw": raw,
"shared_ids": shared_ids,
}
class MountBudget:
def __init__(self, limit: int):
self.limit = limit
self.resident = 0
self.lock = threading.Lock()
def reserve(self, amount: int) -> bool:
with self.lock:
if amount > self.limit - self.resident:
return False
self.resident += amount
return True
def release(self, amount: int) -> None:
with self.lock:
if amount > self.resident:
raise RuntimeError("cache budget underflow")
self.resident -= amount
class BodyCache:
EMPTY = "EMPTY"
INFLIGHT = "INFLIGHT"
READY = "READY"
FAILED = "FAILED"
def __init__(self, spec: dict, budget: MountBudget):
self.limit = spec["model"]["entry_body_limit_bytes"]
self.budget = budget
self.cv = threading.Condition()
self.state = self.EMPTY
self.body: dict | None = None
self.error: Reject | None = None
self.waiters = 0
self.loads = 0
self.charged = 0
self.closing = False
self.wait_for_waiters = 0
def acquire(self, reader: Reader, inode: dict[str, int]) -> dict:
owner = False
reserved = False
with self.cv:
if self.closing:
raise Reject("ENXIO", "cache.closing")
if self.state == self.READY:
if self.body is None:
raise RuntimeError("READY cache has no body")
return self.body
if self.state == self.INFLIGHT:
self.waiters += 1
try:
while self.state == self.INFLIGHT:
self.cv.wait()
if self.state == self.READY:
if self.body is None:
raise RuntimeError("published cache has no body")
return self.body
if self.state != self.FAILED or self.error is None:
raise RuntimeError("cache waiter has no typed result")
raise Reject(self.error.errno_name, self.error.point)
finally:
self.waiters -= 1
if self.state == self.FAILED and self.waiters == 0:
self.error = None
self.state = self.EMPTY
if self.waiters == 0:
self.cv.notify_all()
if self.state == self.FAILED:
if self.waiters:
return reader.load_body(inode, validate_shared=True)
self.error = None
self.state = self.EMPTY
if inode["xattr_size"] <= self.limit:
reserved = self.budget.reserve(inode["xattr_size"])
if not reserved:
return reader.load_body(inode, validate_shared=True)
self.state = self.INFLIGHT
self.charged = inode["xattr_size"]
owner = True
if not owner:
raise RuntimeError("cache claim lost owner state")
try:
if self.wait_for_waiters:
deadline = time.monotonic() + 5
while True:
with self.cv:
if self.waiters >= self.wait_for_waiters:
break
if time.monotonic() >= deadline:
raise RuntimeError("cache waiters did not reach owner barrier")
time.sleep(0.001)
body = reader.load_body(inode, validate_shared=True)
with self.cv:
self.loads += 1
self.body = body
self.error = None
self.state = self.READY
self.cv.notify_all()
return body
except Reject as error:
with self.cv:
self.loads += 1
self.budget.release(self.charged)
self.charged = 0
self.error = error
self.state = self.FAILED
if self.waiters == 0:
self.error = None
self.state = self.EMPTY
self.cv.notify_all()
raise
def invalidate(self) -> None:
with self.cv:
while self.state == self.INFLIGHT or self.waiters:
self.cv.wait()
if self.state == self.READY:
self.body = None
self.budget.release(self.charged)
self.charged = 0
self.error = None
self.state = self.EMPTY
def close(self) -> None:
with self.cv:
self.closing = True
while self.state == self.INFLIGHT or self.waiters:
self.cv.wait()
if self.state == self.READY:
self.body = None
self.budget.release(self.charged)
self.charged = 0
self.error = None
self.state = self.EMPTY
def body_for_operation(
reader: Reader, inode: dict[str, int], variant: str, cache: BodyCache | None
) -> dict:
if variant == "baseline":
return reader.load_body(inode, validate_shared=False)
if variant != "candidate" or cache is None:
raise RuntimeError("invalid B19a oracle variant")
return cache.acquire(reader, inode)
def entries_for_body(reader: Reader, body: dict) -> list[dict]:
entries = list(body["inline"])
entries.extend(reader.shared_entry(shared_id) for shared_id in body["shared_ids"])
return entries
def getxattr(
reader: Reader,
inode: dict[str, int],
name: bytes,
variant: str,
cache: BodyCache | None,
) -> bytes:
body = body_for_operation(reader, inode, variant, cache)
for entry in body["inline"]:
if entry["index"] == 1 and entry["name"] == name:
return bytes(entry["value"])
for shared_id in body["shared_ids"]:
entry = reader.shared_entry(shared_id)
if entry["index"] == 1 and entry["name"] == name:
return bytes(entry["value"])
raise Reject("ENOATTR", "xattr.not-found")
def listxattr(
reader: Reader,
inode: dict[str, int],
variant: str,
cache: BodyCache | None,
) -> list[bytes]:
return [
bytes(entry["name"])
for entry in entries_for_body(
reader, body_for_operation(reader, inode, variant, cache)
)
if entry["index"] == 1
]
def expected_value(label: str, length: int) -> bytes:
seed = (label + "|").encode("ascii")
return (seed * (length // len(seed) + 1))[:length]
def encode_dirblock(names: list[bytes], size: int = 128) -> bytes:
first_name = len(names) * 12
payload = bytearray(size)
cursor = first_name
for index, name in enumerate(names):
if cursor + len(name) > size:
raise ValueError("synthetic directory block is too small")
struct.pack_into("<QHBB", payload, index * 12, index + 1, cursor, 1, 0)
payload[cursor : cursor + len(name)] = name
cursor += len(name)
return bytes(payload)
def expect_reject(function, errno_name: str, point: str) -> dict[str, str]:
try:
function()
except Reject as error:
if error.errno_name != errno_name or error.point != point:
raise SystemExit(
f"oracle control expected {errno_name} at {point}, got {error}"
)
return {"errno": error.errno_name, "point": error.point}
raise SystemExit(f"oracle control unexpectedly passed: {errno_name} at {point}")
def run_selftest(output: Path) -> dict:
controls = {}
first = Reader.parse_dirblock(
encode_dirblock([b"alpha", b"boundary-last"], 96),
0,
)
second = Reader.parse_dirblock(
encode_dirblock([b"target", b"zeta"], 80), 1
)
Reader.validate_boundary(first, second)
combined = first + second
target_nid = Reader.resolve_entries(combined, b"target")
matches = [entry for entry in combined if entry["name"] == b"target"]
if (
len(matches) != 1
or matches[0]["block"] != 1
or int(matches[0]["nid"]) != target_nid
):
raise SystemExit("target was not resolved after a block boundary")
controls["target-after-boundary"] = "PASS"
controls["short-final-block"] = "PASS"
controls["duplicate-within-block"] = expect_reject(
lambda: Reader.parse_dirblock(encode_dirblock([b"dup", b"dup"]), 0),
"EINTEGRITY",
"directory.order",
)
left = Reader.parse_dirblock(encode_dirblock([b"alpha", b"dup"]), 0)
right = Reader.parse_dirblock(encode_dirblock([b"dup", b"zeta"]), 1)
controls["duplicate-across-blocks"] = expect_reject(
lambda: Reader.validate_boundary(left, right),
"EINTEGRITY",
"directory.boundary-order",
)
controls["missing-target"] = expect_reject(
lambda: Reader.resolve_entries(combined, b"missing"),
"ENOENT",
"path.missing",
)
duplicate_entries = combined + [{**combined[-1], "nid": 999, "name": b"target"}]
controls["duplicate-resolution"] = expect_reject(
lambda: Reader.resolve_entries(duplicate_entries, b"target"),
"EINTEGRITY",
"path.duplicate",
)
backwards = Reader.parse_dirblock(encode_dirblock([b"aardvark", b"beta"]), 1)
controls["backwards-boundary"] = expect_reject(
lambda: Reader.validate_boundary(first, backwards),
"EINTEGRITY",
"directory.boundary-order",
)
malformed = bytearray(encode_dirblock([b"alpha", b"bravo"]))
struct.pack_into("<H", malformed, 20, len(malformed) + 1)
controls["crossing-name"] = expect_reject(
lambda: Reader.parse_dirblock(bytes(malformed), 0),
"EINTEGRITY",
"directory.name-bounds",
)
report = {"controls": controls, "status": "PASS"}
output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="ascii")
return report
def operation_result(
image: Path, spec: dict, variant: str, operation: str
) -> dict[str, object]:
budget = MountBudget(spec["model"]["cache_budget_bytes"])
cache = BodyCache(spec, budget) if variant == "candidate" else None
try:
with Reader(image) as reader:
inode = reader.resolve(spec["fixture"]["target"])
reader.stats.reset()
if operation == "get-inline":
value = getxattr(reader, inode, b"inline", variant, cache)
return {"status": "PASS", "value_sha256": hashlib.sha256(value).hexdigest()}
if operation == "get-shared":
value = getxattr(reader, inode, b"shared", variant, cache)
return {"status": "PASS", "value_sha256": hashlib.sha256(value).hexdigest()}
if operation == "list-user":
names = listxattr(reader, inode, variant, cache)
return {"names": [name.decode("ascii") for name in names], "status": "PASS"}
raise RuntimeError(f"unknown operation: {operation}")
except Reject as error:
return {"errno": spec["errno"].get(error.errno_name, -1), "point": error.point, "status": error.errno_name}
finally:
if cache is not None:
cache.close()
if budget.resident != 0:
raise RuntimeError("operation cache budget leaked")
def run_correctness(fixtures: Path, spec: dict, output: Path) -> dict:
valid = fixtures / "valid.erofs"
with Reader(valid) as reader:
root = reader.inode(reader.root_nid)
root_entries = reader.directory_entries(root)
target_entries = [entry for entry in root_entries if entry["name"] == b"target.bin"]
if len(target_entries) != 1 or int(target_entries[0]["block"]) == 0:
raise SystemExit("real target did not resolve after the first block")
if [entry["name"] for entry in root_entries] != sorted(
entry["name"] for entry in root_entries
):
raise SystemExit("real directory order is unstable")
boundary = []
for left, right in zip(root_entries, root_entries[1:]):
if left["block"] != right["block"]:
boundary.append(
{
"left": bytes(left["name"]).decode("ascii"),
"right": bytes(right["name"]).decode("ascii"),
}
)
inode = reader.resolve(spec["fixture"]["target"])
body = reader.load_body(inode, validate_shared=True)
if not body["inline"] or not body["shared_ids"]:
raise SystemExit("real target lacks inline/shared xattr coverage")
directory = {
"block_count": (root["size"] + reader.block_size - 1) // reader.block_size,
"boundaries": boundary,
"entry_count": len(root_entries),
"target_block": target_entries[0]["block"],
"target_entry": target_entries[0]["entry"],
}
expected_inline = expected_value(
"inline-value-p15-022", spec["fixture"]["inline_value_bytes"]
)
expected_shared = expected_value(
"shared-value-p15-022", spec["fixture"]["shared_value_bytes"]
)
cases = []
for variant in ("baseline", "candidate"):
inline = operation_result(valid, spec, variant, "get-inline")
shared = operation_result(valid, spec, variant, "get-shared")
listed = operation_result(valid, spec, variant, "list-user")
if inline.get("value_sha256") != hashlib.sha256(expected_inline).hexdigest():
raise SystemExit(f"{variant} inline value mismatch")
if shared.get("value_sha256") != hashlib.sha256(expected_shared).hexdigest():
raise SystemExit(f"{variant} shared value mismatch")
if sorted(listed.get("names", [])) != ["inline", "shared"]:
raise SystemExit(f"{variant} list result mismatch: {listed}")
cases.extend(
[
{"id": "valid-inline", "variant": variant, "result": inline},
{"id": "valid-shared", "variant": variant, "result": shared},
{"id": "valid-list", "variant": variant, "result": listed},
]
)
damaged = [
("corrupt-shared-count.erofs", "get-inline", "EINTEGRITY"),
("corrupt-shared-id.erofs", "get-shared", "EINTEGRITY"),
("corrupt-inline-name.erofs", "list-user", "EINTEGRITY"),
]
for filename, operation, expected in damaged:
for variant in ("baseline", "candidate"):
result = operation_result(fixtures / filename, spec, variant, operation)
if result["status"] != expected or int(result.get("errno", -1)) < 0:
raise SystemExit(f"{filename} {variant} mismatch: {result}")
cases.append({"id": filename, "variant": variant, "result": result})
budget = MountBudget(spec["model"]["cache_budget_bytes"])
cache = BodyCache(spec, budget)
cache.wait_for_waiters = spec["concurrency"]["workers"] - 1
with Reader(valid) as reader:
inode = reader.resolve(spec["fixture"]["target"])
barrier = threading.Barrier(spec["concurrency"]["workers"])
def worker(_: int) -> str:
barrier.wait()
value = getxattr(reader, inode, b"inline", "candidate", cache)
return hashlib.sha256(value).hexdigest()
with ThreadPoolExecutor(max_workers=spec["concurrency"]["workers"]) as executor:
hashes = list(executor.map(worker, range(spec["concurrency"]["workers"])))
if len(set(hashes)) != 1 or cache.loads != 1 or cache.state != BodyCache.READY:
raise SystemExit("candidate owner/waiter publication mismatch")
first_charge = budget.resident
cache.invalidate()
if budget.resident != 0 or cache.state != BodyCache.EMPTY:
raise SystemExit("candidate invalidation did not release cache")
cache.wait_for_waiters = 0
getxattr(reader, inode, b"inline", "candidate", cache)
if cache.loads != 2 or budget.resident != first_charge:
raise SystemExit("candidate re-establishment mismatch")
cache.close()
if budget.resident != 0:
raise SystemExit("candidate reclaim/close leaked budget")
report = {
"cases": cases,
"concurrency": {
"body_loads": 1,
"completed": spec["concurrency"]["workers"],
"workers": spec["concurrency"]["workers"],
},
"directory": directory,
"fixture_sha256": {
path.name: sha256(path) for path in sorted(fixtures.glob("*.erofs"))
},
"lifecycle": {
"establish": "PASS",
"hit": "PASS",
"invalidate": "PASS",
"reclaim": "PASS",
"resident_after": budget.resident,
},
"status": "PASS",
}
output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="ascii")
return report
def run_sample(
image: Path, spec: dict, variant: str, sample: int, output: Path
) -> dict:
budget = MountBudget(spec["model"]["cache_budget_bytes"])
cache = BodyCache(spec, budget) if variant == "candidate" else None
operations = spec["benchmark"]["operations"]
with Reader(image) as reader:
inode = reader.resolve(spec["fixture"]["target"])
def one_loop() -> None:
for operation in operations:
if operation == "get-inline":
getxattr(reader, inode, b"inline", variant, cache)
elif operation == "get-shared":
getxattr(reader, inode, b"shared", variant, cache)
elif operation == "list-user":
listxattr(reader, inode, variant, cache)
else:
raise RuntimeError(f"unknown benchmark operation: {operation}")
for _ in range(spec["benchmark"]["warmup_loops"]):
one_loop()
reader.stats.reset()
started = time.monotonic_ns()
for _ in range(spec["benchmark"]["loops_per_sample"]):
one_loop()
elapsed = time.monotonic_ns() - started
reads = reader.stats.snapshot()
loads = cache.loads if cache is not None else 0
resident_before_close = budget.resident
if cache is not None:
cache.close()
if budget.resident != 0:
raise SystemExit("benchmark cache budget leaked")
report = {
"cache_body_loads": loads,
"cache_resident_before_close": resident_before_close,
"elapsed_ns": elapsed,
"fixture_sha256": sha256(image),
"host": os.uname().sysname + " " + os.uname().release,
"loops": spec["benchmark"]["loops_per_sample"],
"operations": operations,
"reads": reads,
"sample": sample,
"status": "PASS",
"variant": variant,
"warmup_loops": spec["benchmark"]["warmup_loops"],
}
output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="ascii")
print(json.dumps(report, sort_keys=True))
return report
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--spec", type=Path, required=True)
subparsers = parser.add_subparsers(dest="command", required=True)
selftest = subparsers.add_parser("selftest")
selftest.add_argument("--output", type=Path, required=True)
correctness = subparsers.add_parser("correctness")
correctness.add_argument("--fixtures", type=Path, required=True)
correctness.add_argument("--output", type=Path, required=True)
sample = subparsers.add_parser("sample")
sample.add_argument("--image", type=Path, required=True)
sample.add_argument("--variant", choices=("baseline", "candidate"), required=True)
sample.add_argument("--sample", type=int, required=True)
sample.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
spec = load_spec(args.spec)
if args.command == "selftest":
report = run_selftest(args.output)
print(json.dumps(report, sort_keys=True))
elif args.command == "correctness":
report = run_correctness(args.fixtures, spec, args.output)
print(json.dumps(report, sort_keys=True))
else:
run_sample(args.image, spec, args.variant, args.sample, args.output)
return 0
if __name__ == "__main__":
raise SystemExit(main())