Skip to content

Commit c922913

Browse files
committed
test: add a test to measure Firecracker memory overhead
Our current binary size test measures space in disk, as a proxy for used memory. This is not accurate, as parts of the binary are not loaded into memory. This commits adds a new test that measures memory overhead directly and published metrics for different guest configurations. Also remove thresholds from test_binary_size.py test, as we measure memory overhead directly now. Signed-off-by: Pablo Barbáchano <[email protected]>
1 parent 210cf6e commit c922913

File tree

2 files changed

+104
-82
lines changed

2 files changed

+104
-82
lines changed
Lines changed: 12 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,29 @@
11
# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
# SPDX-License-Identifier: Apache-2.0
3-
"""Tests that check if the release binary sizes fall within expected size."""
3+
"""Tests that check if the release binary sizes fall within expected size.
4+
5+
This is not representative of the actual memory overhead of Firecracker.
6+
7+
A more representative test is file:../performance/test_memory_overhead.py
8+
"""
49

5-
import os
610
import platform
711

812
import pytest
913

1014
import host_tools.cargo_build as host
1115

1216
MACHINE = platform.machine()
13-
""" Platform definition used to select the correct size target"""
14-
15-
SIZES_DICT = {
16-
"x86_64": {
17-
"FC_BINARY_SIZE_TARGET": 3063120,
18-
"JAILER_BINARY_SIZE_TARGET": 1080088,
19-
},
20-
"aarch64": {
21-
"FC_BINARY_SIZE_TARGET": 2441008,
22-
"JAILER_BINARY_SIZE_TARGET": 1040928,
23-
},
24-
}
25-
26-
FC_BINARY_SIZE_TARGET = SIZES_DICT[MACHINE]["FC_BINARY_SIZE_TARGET"]
27-
"""Firecracker target binary size in bytes"""
28-
29-
JAILER_BINARY_SIZE_TARGET = SIZES_DICT[MACHINE]["JAILER_BINARY_SIZE_TARGET"]
30-
"""Jailer target binary size in bytes"""
31-
32-
BINARY_SIZE_TOLERANCE = 0.05
33-
"""Tolerance of 5% allowed for binary size"""
3417

3518

3619
@pytest.mark.timeout(500)
3720
def test_firecracker_binary_size(record_property, metrics):
3821
"""
3922
Test if the size of the firecracker binary is within expected ranges.
4023
"""
41-
fc_binary, _ = host.get_firecracker_binaries()
42-
43-
result = check_binary_size(
44-
"firecracker",
45-
fc_binary,
46-
FC_BINARY_SIZE_TARGET,
47-
BINARY_SIZE_TOLERANCE,
48-
)
49-
50-
record_property(
51-
"firecracker_binary_size",
52-
f"{result}B ({FC_BINARY_SIZE_TARGET}B ±{BINARY_SIZE_TOLERANCE:.0%})",
53-
)
24+
fc_binary = host.get_binary("firecracker")
25+
result = fc_binary.stat().st_size
26+
record_property("firecracker_binary_size", f"{result}B")
5427
metrics.set_dimensions({"cpu_arch": MACHINE})
5528
metrics.put_metric("firecracker_binary_size", result, unit="Bytes")
5629

@@ -60,51 +33,8 @@ def test_jailer_binary_size(record_property, metrics):
6033
"""
6134
Test if the size of the jailer binary is within expected ranges.
6235
"""
63-
_, jailer_binary = host.get_firecracker_binaries()
64-
65-
result = check_binary_size(
66-
"jailer",
67-
jailer_binary,
68-
JAILER_BINARY_SIZE_TARGET,
69-
BINARY_SIZE_TOLERANCE,
70-
)
71-
72-
record_property(
73-
"jailer_binary_size",
74-
f"{result}B ({JAILER_BINARY_SIZE_TARGET}B ±{BINARY_SIZE_TOLERANCE:.0%})",
75-
)
36+
jailer_binary = host.get_binary("jailer")
37+
result = jailer_binary.stat().st_size
38+
record_property("jailer_binary_size", f"{result}B")
7639
metrics.set_dimensions({"cpu_arch": MACHINE})
7740
metrics.put_metric("jailer_binary_size", result, unit="Bytes")
78-
79-
80-
def check_binary_size(name, binary_path, size_target, tolerance):
81-
"""Check if the specified binary falls within the expected range."""
82-
# Get the size of the release binary.
83-
binary_size = os.path.getsize(binary_path)
84-
85-
# Get the name of the variable that needs updating.
86-
namespace = globals()
87-
size_target_name = [name for name in namespace if namespace[name] is size_target][0]
88-
89-
# Compute concrete binary size difference.
90-
delta_size = size_target - binary_size
91-
92-
binary_low_msg = (
93-
"Current {} binary size of {} bytes is below the target"
94-
" of {} bytes with {} bytes.\n"
95-
"Update the {} threshold".format(
96-
name, binary_size, size_target, delta_size, size_target_name
97-
)
98-
)
99-
100-
assert binary_size > size_target * (1 - tolerance), binary_low_msg
101-
102-
binary_high_msg = (
103-
"Current {} binary size of {} bytes is above the target"
104-
" of {} bytes with {} bytes.\n".format(
105-
name, binary_size, size_target, -delta_size
106-
)
107-
)
108-
109-
assert binary_size < size_target * (1 + tolerance), binary_high_msg
110-
return binary_size
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""Track Firecracker memory overhead
5+
6+
Because Firecracker is a static binary, and is copied before execution, no
7+
memory is shared across many different processes. It is thus important to track
8+
how much memory overhead Firecracker adds.
9+
10+
These tests output metrics to capture the memory overhead that Firecracker adds,
11+
both from the binary file (file-backed) and what Firecracker allocates during
12+
the process lifetime.
13+
14+
The memory overhead of the jailer is not important as it is short-lived.
15+
"""
16+
17+
from collections import defaultdict
18+
from pathlib import Path
19+
20+
import psutil
21+
import pytest
22+
23+
from framework.properties import global_props
24+
25+
# If guest memory is >3328MB, it is split in a 2nd region
26+
X86_MEMORY_GAP_START = 3328 * 2**20
27+
28+
29+
@pytest.mark.parametrize(
30+
"vcpu_count,mem_size_mib",
31+
[(1, 128), (1, 1024), (2, 2024), (4, 4096)],
32+
)
33+
def test_memory_overhead(
34+
microvm_factory, guest_kernel, rootfs, vcpu_count, mem_size_mib, metrics
35+
):
36+
"""Track Firecracker memory overhead.
37+
38+
We take a single measurement as it only varies by a few KiB each run.
39+
"""
40+
41+
microvm = microvm_factory.build(guest_kernel, rootfs)
42+
microvm.spawn()
43+
microvm.basic_config(vcpu_count=vcpu_count, mem_size_mib=mem_size_mib)
44+
microvm.add_net_iface()
45+
microvm.start()
46+
47+
# check that the vm is running
48+
microvm.ssh.run("true")
49+
50+
guest_mem_bytes = mem_size_mib * 2**20
51+
guest_mem_splits = {
52+
guest_mem_bytes,
53+
X86_MEMORY_GAP_START,
54+
}
55+
if guest_mem_bytes > X86_MEMORY_GAP_START:
56+
guest_mem_splits.add(guest_mem_bytes - X86_MEMORY_GAP_START)
57+
58+
mem_stats = defaultdict(int)
59+
mem_stats["guest_memory"] = guest_mem_bytes
60+
ps = psutil.Process(microvm.jailer_clone_pid)
61+
62+
for pmmap in ps.memory_maps(grouped=False):
63+
# We publish 'size' and 'rss' (resident). size would be the worst case,
64+
# whereas rss is the current paged-in memory.
65+
66+
mem_stats["total_size"] += pmmap.size
67+
mem_stats["total_rss"] += pmmap.rss
68+
pmmap_path = Path(pmmap.path)
69+
if pmmap_path.exists() and pmmap_path.name.startswith("firecracker"):
70+
mem_stats["binary_size"] += pmmap.size
71+
mem_stats["binary_rss"] += pmmap.rss
72+
73+
if pmmap.size not in guest_mem_splits:
74+
mem_stats["overhead_size"] += pmmap.size
75+
mem_stats["overhead_rss"] += pmmap.rss
76+
77+
dimensions = {
78+
# "instance": global_props.instance,
79+
# "cpu_model": global_props.cpu_model,
80+
"architecture": global_props.cpu_architecture,
81+
"host_kernel": "linux-" + global_props.host_linux_version,
82+
"guest_kernel": guest_kernel.name,
83+
"rootfs": rootfs.name,
84+
}
85+
metrics.set_dimensions(dimensions)
86+
for key, value in mem_stats.items():
87+
metrics.put_metric(key, value, unit="Bytes")
88+
89+
mem_info = ps.memory_full_info()
90+
for metric in ["uss", "text"]:
91+
val = getattr(mem_info, metric)
92+
metrics.put_metric(metric, val, unit="Bytes")

0 commit comments

Comments
 (0)