Skip to content

Commit 43fdded

Browse files
committed
kerf: Compute pool availability from /proc/iomem in kerf show
The baseline device tree only snapshots the memory pool at init time, so kerf show kept printing the full pool size even after instances allocated memory from it. With lazy_cma the pool and every allocation carved out of it are registered in /proc/iomem, so use that as the single source of truth. Centralize /proc/iomem parsing in resources.py with a shared line parser, a canonical pool lookup keyed on MULTIKERNEL_POOL_NAME, and a new get_pool_allocated_bytes() that sums all regions nested inside the pool range, merging overlapping and nested intervals to avoid double counting. Replace the duplicated pool-lookup copies in init and the dtc validator with the canonical helper; the validator's old copy matched any line containing "multikernel" and could grab the wrong region. kerf show now prints Pool Allocated and Pool Available lines derived from /proc/iomem, falling back to the baseline snapshot when the pool is not registered. Signed-off-by: Cong Wang <cwang@multikernel.io>
1 parent 1cde6cc commit 43fdded

5 files changed

Lines changed: 195 additions & 83 deletions

File tree

‎src/kerf/dtc/validator.py‎

Lines changed: 3 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@
1818

1919
import re
2020
from pathlib import Path
21-
from typing import List, Dict, Tuple, Optional
21+
from typing import List, Dict, Optional
2222
from ..models import GlobalDeviceTree, ValidationResult, ResourceUsage
23+
from ..resources import get_memory_pool_from_iomem
2324

2425

2526
class MultikernelValidator:
@@ -188,32 +189,6 @@ def _get_system_physical_memory(self) -> Optional[int]:
188189

189190
return None
190191

191-
def _get_multikernel_memory_pool_from_iomem(self) -> Optional[Tuple[int, int]]:
192-
"""
193-
Get multikernel memory pool region from /proc/iomem.
194-
Returns (base_address, size_bytes) or None if not found.
195-
196-
Expected format: "40000000-7fefffff : Multikernel Memory Pool"
197-
"""
198-
try:
199-
iomem_path = Path("/proc/iomem")
200-
if not iomem_path.exists():
201-
return None
202-
with open(iomem_path, "r", encoding="utf-8") as f:
203-
for line in f:
204-
if "multikernel" in line.lower():
205-
match = re.search(r"([0-9a-fA-F]+)-([0-9a-fA-F]+)", line)
206-
if match:
207-
base = int(match.group(1), 16)
208-
end = int(match.group(2), 16)
209-
# Size is end - start + 1 (inclusive range)
210-
size = end - base + 1
211-
return (base, size)
212-
except (OSError, IOError, ValueError):
213-
pass
214-
215-
return None
216-
217192
def _validate_hardware_inventory(self, tree: GlobalDeviceTree):
218193
"""Validate hardware inventory consistency and against running system."""
219194
cpus = tree.hardware.cpus
@@ -285,7 +260,7 @@ def _validate_hardware_inventory(self, tree: GlobalDeviceTree):
285260
if memory.memory_pool_bytes <= 0:
286261
self.errors.append("Hardware inventory: Spawn pool size must be positive")
287262

288-
iomem_pool = self._get_multikernel_memory_pool_from_iomem()
263+
iomem_pool = get_memory_pool_from_iomem()
289264
if iomem_pool is None or (
290265
memory.memory_pool_base != iomem_pool[0] or memory.memory_pool_bytes != iomem_pool[1]
291266
):

‎src/kerf/init/main.py‎

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
import re
2626
import sys
2727
from pathlib import Path
28-
from typing import Optional, Tuple
28+
from typing import Optional
2929

3030
import click
3131
import libfdt
@@ -39,6 +39,7 @@
3939
from ..create.main import parse_cpu_spec, parse_device_list, parse_memory_spec
4040
from ..dtc.parser import DeviceTreeParser
4141
from ..lazy_cma import LAZY_CMA_DEVICE, allocate_multikernel_pool
42+
from ..resources import get_memory_pool_from_iomem
4243
from ..dtc.reporter import ValidationReporter
4344
from ..dtc.validator import MultikernelValidator
4445
from ..exceptions import KernelInterfaceError, ParseError, ValidationError
@@ -121,30 +122,6 @@ def mount_multikernel_fs(verbose: bool = False) -> None:
121122
click.echo("✓ Successfully mounted multikernel filesystem")
122123

123124

124-
def get_multikernel_memory_pool_from_iomem() -> Optional[Tuple[int, int]]:
125-
"""
126-
Get multikernel memory pool region from /proc/iomem.
127-
Returns (base_address, size_bytes) or None if not found.
128-
"""
129-
try:
130-
iomem_path = Path('/proc/iomem')
131-
if not iomem_path.exists():
132-
return None
133-
with open(iomem_path, 'r', encoding='utf-8') as f:
134-
for line in f:
135-
if 'Multikernel Memory Pool' in line:
136-
match = re.search(r'([0-9a-fA-F]+)-([0-9a-fA-F]+)', line)
137-
if match:
138-
base = int(match.group(1), 16)
139-
end = int(match.group(2), 16)
140-
size = end - base + 1
141-
return (base, size)
142-
except (OSError, IOError, ValueError):
143-
pass
144-
145-
return None
146-
147-
148125
def get_total_memory_from_system() -> Optional[int]:
149126
"""
150127
Get total system memory from /proc/meminfo.
@@ -433,7 +410,7 @@ def build_baseline_from_cmdline(
433410
host_reserved_cpus = [0]
434411
cpu_list = sorted(list(available_cpus))
435412

436-
memory_pool = get_multikernel_memory_pool_from_iomem()
413+
memory_pool = get_memory_pool_from_iomem()
437414
if memory_pool is None:
438415
if not memory:
439416
raise KernelInterfaceError(
@@ -451,7 +428,7 @@ def build_baseline_from_cmdline(
451428

452429
# Re-read /proc/iomem so the baseline reflects exactly what the
453430
# kernel registered for the allocation.
454-
memory_pool = get_multikernel_memory_pool_from_iomem() or (pool_base, pool_bytes)
431+
memory_pool = get_memory_pool_from_iomem() or (pool_base, pool_bytes)
455432
elif memory:
456433
click.echo(
457434
"Note: multikernel memory pool already exists in /proc/iomem; "

‎src/kerf/resources.py‎

Lines changed: 90 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,91 @@
2020
creating or updating kernel instances.
2121
"""
2222

23-
from typing import List, Set, Optional
23+
import re
24+
from pathlib import Path
25+
from typing import List, Set, Optional, Tuple
26+
27+
from .lazy_cma import MULTIKERNEL_POOL_NAME
2428
from .models import GlobalDeviceTree
2529
from .exceptions import ResourceError
2630

31+
IOMEM_PATH = "/proc/iomem"
32+
33+
_IOMEM_RANGE_RE = re.compile(r"([0-9a-fA-F]+)-([0-9a-fA-F]+)\s*:\s*(.*)")
34+
35+
36+
def _parse_iomem_regions(iomem_path: str) -> List[Tuple[int, int, str]]:
37+
"""Return (base, end, name) tuples for every range line in /proc/iomem."""
38+
regions = []
39+
try:
40+
path = Path(iomem_path)
41+
if not path.exists():
42+
return regions
43+
with open(path, "r", encoding="utf-8") as f:
44+
for line in f:
45+
match = _IOMEM_RANGE_RE.search(line)
46+
if match:
47+
base = int(match.group(1), 16)
48+
end = int(match.group(2), 16)
49+
regions.append((base, end, match.group(3).strip()))
50+
except (OSError, IOError, ValueError):
51+
pass
52+
return regions
53+
54+
55+
def get_memory_pool_from_iomem(iomem_path: str = IOMEM_PATH) -> Optional[Tuple[int, int]]:
56+
"""
57+
Get the multikernel memory pool region from /proc/iomem.
58+
59+
Returns:
60+
(base_address, size_bytes) or None if the pool is not registered
61+
"""
62+
for base, end, name in _parse_iomem_regions(iomem_path):
63+
if MULTIKERNEL_POOL_NAME in name:
64+
return (base, end - base + 1)
65+
return None
66+
67+
68+
def get_pool_allocated_bytes(iomem_path: str = IOMEM_PATH) -> Optional[Tuple[int, int, int]]:
69+
"""
70+
Compute pool usage from /proc/iomem, the single source of truth.
71+
72+
Every region nested inside the pool range (instance memory, daxfs
73+
heaps, ...) counts as allocated. Overlapping and nested child regions
74+
are merged so nothing is double counted.
75+
76+
Returns:
77+
(pool_base, pool_bytes, allocated_bytes), or None if the pool is
78+
not registered in /proc/iomem
79+
"""
80+
pool = get_memory_pool_from_iomem(iomem_path)
81+
if pool is None:
82+
return None
83+
pool_base, pool_bytes = pool
84+
pool_end = pool_base + pool_bytes - 1
85+
86+
children = []
87+
for base, end, name in _parse_iomem_regions(iomem_path):
88+
if MULTIKERNEL_POOL_NAME in name:
89+
continue
90+
if base >= pool_base and end <= pool_end:
91+
children.append((base, end))
92+
93+
allocated = 0
94+
current_base = current_end = None
95+
for base, end in sorted(children):
96+
if current_base is None:
97+
current_base, current_end = base, end
98+
elif base <= current_end + 1:
99+
current_end = max(current_end, end)
100+
else:
101+
allocated += current_end - current_base + 1
102+
current_base, current_end = base, end
103+
if current_base is not None:
104+
allocated += current_end - current_base + 1
105+
106+
return (pool_base, pool_bytes, allocated)
107+
27108

28109
def get_available_cpus(tree: GlobalDeviceTree) -> Set[int]:
29110
"""
@@ -62,7 +143,9 @@ def get_allocated_cpus(tree: GlobalDeviceTree) -> Set[int]:
62143
return allocated
63144

64145

65-
def get_allocated_memory_regions_from_iomem() -> List[tuple[int, int]]:
146+
def get_allocated_memory_regions_from_iomem(
147+
iomem_path: str = IOMEM_PATH,
148+
) -> List[tuple[int, int]]:
66149
"""
67150
Get list of allocated memory regions from /proc/iomem.
68151
@@ -72,28 +155,11 @@ def get_allocated_memory_regions_from_iomem() -> List[tuple[int, int]]:
72155
Returns:
73156
List of (base_address, size_bytes) tuples
74157
"""
75-
regions = []
76-
try:
77-
from pathlib import Path
78-
import re
79-
80-
iomem_path = Path("/proc/iomem")
81-
if not iomem_path.exists():
82-
return regions
83-
84-
with open(iomem_path, "r", encoding="utf-8") as f:
85-
for line in f:
86-
if "mk-instance-" in line:
87-
match = re.search(r"([0-9a-fA-F]+)-([0-9a-fA-F]+)", line)
88-
if match:
89-
base = int(match.group(1), 16)
90-
end = int(match.group(2), 16)
91-
size = end - base + 1 # Inclusive range
92-
regions.append((base, size))
93-
except (OSError, IOError, ValueError):
94-
pass
95-
96-
return regions
158+
return [
159+
(base, end - base + 1)
160+
for base, end, name in _parse_iomem_regions(iomem_path)
161+
if "mk-instance-" in name
162+
]
97163

98164

99165
def get_allocated_memory_regions(tree: GlobalDeviceTree) -> List[tuple[int, int]]:

‎src/kerf/show/main.py‎

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from ..exceptions import KernelInterfaceError, ParseError
3434
from ..metadata import load_instance_metadata
3535
from ..models import GlobalDeviceTree
36+
from ..resources import get_pool_allocated_bytes
3637
from ..utils import get_instance_id_from_name, get_instance_status
3738

3839

@@ -295,14 +296,32 @@ def display_baseline_info(tree: GlobalDeviceTree, verbose: bool = False):
295296
click.echo("\n Memory:")
296297
total_gb = hardware.memory.total_bytes / (1024**3)
297298
reserved_gb = hardware.memory.host_reserved_bytes / (1024**3)
298-
pool_gb = hardware.memory.memory_pool_bytes / (1024**3)
299299
click.echo(f" Total: {total_gb:.2f} GB ({hardware.memory.total_bytes} bytes)")
300300
click.echo(
301301
f" Host Reserved: {reserved_gb:.2f} GB ({hardware.memory.host_reserved_bytes} bytes)"
302302
)
303-
click.echo(f" Pool Base: 0x{hardware.memory.memory_pool_base:x}")
304-
click.echo(f" Pool Size: {pool_gb:.2f} GB ({hardware.memory.memory_pool_bytes} bytes)")
305-
click.echo(f" Pool End: 0x{hardware.memory.memory_pool_end:x}")
303+
304+
# /proc/iomem is the source of truth for the lazy_cma pool and the
305+
# instance allocations carved out of it; the baseline tree only
306+
# snapshots the pool at init time.
307+
usage = get_pool_allocated_bytes()
308+
if usage is not None:
309+
pool_base, pool_bytes, allocated_bytes = usage
310+
else:
311+
pool_base = hardware.memory.memory_pool_base
312+
pool_bytes = hardware.memory.memory_pool_bytes
313+
allocated_bytes = None
314+
315+
pool_gb = pool_bytes / (1024**3)
316+
click.echo(f" Pool Base: 0x{pool_base:x}")
317+
click.echo(f" Pool Size: {pool_gb:.2f} GB ({pool_bytes} bytes)")
318+
click.echo(f" Pool End: 0x{pool_base + pool_bytes:x}")
319+
if allocated_bytes is not None:
320+
available_bytes = pool_bytes - allocated_bytes
321+
allocated_gb = allocated_bytes / (1024**3)
322+
available_gb = available_bytes / (1024**3)
323+
click.echo(f" Pool Allocated: {allocated_gb:.2f} GB ({allocated_bytes} bytes)")
324+
click.echo(f" Pool Available: {available_gb:.2f} GB ({available_bytes} bytes)")
306325

307326
# NUMA Topology
308327
if hardware.topology and hardware.topology.numa_nodes:

‎tests/test_resources.py‎

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
get_available_cpus,
2222
get_allocated_cpus,
2323
get_allocated_memory_regions,
24+
get_allocated_memory_regions_from_iomem,
25+
get_memory_pool_from_iomem,
26+
get_pool_allocated_bytes,
2427
find_available_memory_base,
2528
validate_cpu_allocation,
2629
validate_memory_allocation,
@@ -228,3 +231,75 @@ def test_find_next_instance_id_full(self, sample_hardware):
228231

229232
with pytest.raises(ResourceError, match="No available instance IDs"):
230233
find_next_instance_id(tree)
234+
235+
236+
class TestIomemPoolAccounting:
237+
"""Test /proc/iomem based pool accounting."""
238+
239+
SAMPLE_IOMEM = "\n".join(
240+
[
241+
"00001000-0009ffff : System RAM",
242+
"40000000-7fffffff : Multikernel Memory Pool",
243+
" 40000000-43ffffff : mk-instance-1-web-server-region-0",
244+
" 44000000-4bffffff : mk-instance-2-database-region-0",
245+
" 4c000000-4fffffff : daxfs",
246+
"80000000-8fffffff : PCI Bus 0000:00",
247+
]
248+
)
249+
250+
@pytest.fixture
251+
def iomem_file(self, tmp_path):
252+
path = tmp_path / "iomem"
253+
path.write_text(self.SAMPLE_IOMEM + "\n", encoding="utf-8")
254+
return str(path)
255+
256+
def test_get_memory_pool_from_iomem(self, iomem_file):
257+
pool = get_memory_pool_from_iomem(iomem_file)
258+
assert pool == (0x40000000, 0x40000000)
259+
260+
def test_get_memory_pool_from_iomem_missing(self, tmp_path):
261+
path = tmp_path / "iomem"
262+
path.write_text("00001000-0009ffff : System RAM\n", encoding="utf-8")
263+
assert get_memory_pool_from_iomem(str(path)) is None
264+
assert get_memory_pool_from_iomem(str(tmp_path / "absent")) is None
265+
266+
def test_get_allocated_memory_regions_from_iomem(self, iomem_file):
267+
regions = get_allocated_memory_regions_from_iomem(iomem_file)
268+
assert regions == [
269+
(0x40000000, 0x4000000),
270+
(0x44000000, 0x8000000),
271+
]
272+
273+
def test_get_pool_allocated_bytes(self, iomem_file):
274+
usage = get_pool_allocated_bytes(iomem_file)
275+
# Instance regions plus the daxfs region nested in the pool
276+
assert usage == (0x40000000, 0x40000000, 0x10000000)
277+
278+
def test_get_pool_allocated_bytes_empty_pool(self, tmp_path):
279+
path = tmp_path / "iomem"
280+
path.write_text(
281+
"40000000-7fffffff : Multikernel Memory Pool\n", encoding="utf-8"
282+
)
283+
assert get_pool_allocated_bytes(str(path)) == (0x40000000, 0x40000000, 0)
284+
285+
def test_get_pool_allocated_bytes_no_pool(self, tmp_path):
286+
path = tmp_path / "iomem"
287+
path.write_text("00001000-0009ffff : System RAM\n", encoding="utf-8")
288+
assert get_pool_allocated_bytes(str(path)) is None
289+
290+
def test_get_pool_allocated_bytes_merges_nested_regions(self, tmp_path):
291+
path = tmp_path / "iomem"
292+
path.write_text(
293+
"\n".join(
294+
[
295+
"40000000-7fffffff : Multikernel Memory Pool",
296+
" 40000000-43ffffff : mk-instance-1-a-region-0",
297+
" 40000000-40ffffff : mk-instance-1-a-subregion",
298+
" 44000000-47ffffff : mk-instance-2-b-region-0",
299+
]
300+
)
301+
+ "\n",
302+
encoding="utf-8",
303+
)
304+
usage = get_pool_allocated_bytes(str(path))
305+
assert usage == (0x40000000, 0x40000000, 0x8000000)

0 commit comments

Comments
 (0)