Skip to content

Commit 9ca6ece

Browse files
committed
feat(init): per-phase trace, batched disk resolve, bounded NIC wait
sandbox.trace=1 emits one deferred pre-handoff line with cumulative Instant-based µs marks (resolve/erofs/cow/overlay/net/switch) — a single console write so tracing doesn't perturb what it measures, and ns resolution where /proc/uptime's 10ms granularity is useless. Disk resolution now builds the serial map in one sysfs sweep per poll covering all layers + COW. NIC MAC waits get their own 200ms budget: missing NICs degrade to the DHCP fallback instead of borrowing the 10s disk timeout and stalling rootfs handoff.
1 parent 3087231 commit 9ca6ece

2 files changed

Lines changed: 106 additions & 28 deletions

File tree

boot/init/src/boot.rs

Lines changed: 93 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ const LAYER_DIR: &str = "/l";
1111
const COW_DIR: &str = "/cow";
1212
const NEWROOT: &str = "/newroot";
1313
const POLL_INTERVAL: Duration = Duration::from_millis(2);
14+
// NICs probe in single-digit ms; a missing one must degrade to the DHCP
15+
// fallback, not stall rootfs handoff for the full disk budget (10s).
16+
const NIC_TIMEOUT: Duration = Duration::from_millis(200);
1417

1518
pub fn run() -> ! {
1619
// Best-effort: if devtmpfs fails there is no console either; later
@@ -35,10 +38,16 @@ pub fn run() -> ! {
3538
Ok(cfg) => cfg,
3639
Err(err) => sys::fatal(&err, cfg::debug_requested(&cmdline)),
3740
};
38-
if let Err(err) = assemble(&cfg) {
41+
let mut marks = Marks::new();
42+
if let Err(err) = assemble(&cfg, &mut marks) {
3943
sys::fatal(&err, cfg.debug);
4044
}
4145

46+
// One deferred trace line (µs, cumulative since sandbox-init start):
47+
// per-phase console writes would perturb exactly what they measure.
48+
if cfg.trace {
49+
println!("sandbox-init: trace{}", marks.render());
50+
}
4251
// Single marker line; boot-bench.sh keys on it. Uptime is kernel-relative,
4352
// directly comparable with printk timestamps on the serial log.
4453
println!(
@@ -50,21 +59,54 @@ pub fn run() -> ! {
5059
sys::fatal(&format!("exec {}: {err}", cfg.init), cfg.debug)
5160
}
5261

53-
fn assemble(cfg: &BootCfg) -> Result<(), String> {
54-
let mut lower = Vec::with_capacity(cfg.layers.len());
55-
for (i, id) in cfg.layers.iter().enumerate() {
56-
let dev = resolve_disk(id, cfg.timeout)?;
62+
/// Cumulative µs checkpoints since sandbox-init start.
63+
struct Marks {
64+
start: Instant,
65+
points: Vec<(&'static str, u128)>,
66+
}
67+
68+
impl Marks {
69+
fn new() -> Self {
70+
Marks {
71+
start: Instant::now(),
72+
points: Vec::new(),
73+
}
74+
}
75+
76+
fn mark(&mut self, label: &'static str) {
77+
self.points.push((label, self.start.elapsed().as_micros()));
78+
}
79+
80+
fn render(&self) -> String {
81+
let mut out = String::new();
82+
for (label, us) in &self.points {
83+
out.push_str(&format!(" {label}@{us}us"));
84+
}
85+
out
86+
}
87+
}
88+
89+
fn assemble(cfg: &BootCfg, marks: &mut Marks) -> Result<(), String> {
90+
let mut ids: Vec<String> = cfg.layers.clone();
91+
ids.push(cfg.cow.clone());
92+
let mut devs = resolve_disks(&ids, cfg.timeout)?;
93+
let cow_dev = devs.pop().expect("cow id was pushed above");
94+
marks.mark("resolve");
95+
96+
let mut lower = Vec::with_capacity(devs.len());
97+
for (i, dev) in devs.iter().enumerate() {
5798
let mnt = format!("{LAYER_DIR}/{i}");
5899
mkdir_all(&mnt)?;
59-
sys::mount(&dev, &mnt, Some("erofs"), libc::MS_RDONLY, None)?;
100+
sys::mount(dev, &mnt, Some("erofs"), libc::MS_RDONLY, None)?;
60101
lower.push(mnt);
61102
}
103+
marks.mark("erofs");
62104

63-
let cow_dev = resolve_disk(&cfg.cow, cfg.timeout)?;
64105
mkdir_all(COW_DIR)?;
65106
sys::mount(&cow_dev, COW_DIR, Some("ext4"), libc::MS_NOATIME, None)?;
66107
mkdir_all(&format!("{COW_DIR}/upper"))?;
67108
mkdir_all(&format!("{COW_DIR}/work"))?;
109+
marks.mark("cow");
68110

69111
mkdir_all(NEWROOT)?;
70112
sys::mount(
@@ -74,6 +116,7 @@ fn assemble(cfg: &BootCfg) -> Result<(), String> {
74116
0,
75117
Some(&cfg::overlay_data(&lower, COW_DIR)),
76118
)?;
119+
marks.mark("overlay");
77120

78121
if let Some(hostname) = &cfg.hostname {
79122
sys::sethostname(hostname)?;
@@ -82,14 +125,17 @@ fn assemble(cfg: &BootCfg) -> Result<(), String> {
82125
let _ = fs::write(format!("{NEWROOT}/etc/machine-id"), "");
83126

84127
persist_network(cfg);
128+
marks.mark("net");
85129

86130
for dir in ["dev", "proc", "sys", "run"] {
87131
let _ = fs::create_dir_all(format!("{NEWROOT}/{dir}"));
88132
}
89133
for mnt in ["/dev", "/proc", "/sys"] {
90134
sys::move_mount(mnt, &format!("{NEWROOT}{mnt}"))?;
91135
}
92-
sys::switch_root(NEWROOT)
136+
sys::switch_root(NEWROOT)?;
137+
marks.mark("switch");
138+
Ok(())
93139
}
94140

95141
/// Materializes kernel ip= params (cocoon CNI static flow) as MAC-matched
@@ -107,7 +153,7 @@ fn persist_network(cfg: &BootCfg) {
107153
return;
108154
}
109155
for ip in &cfg.ips {
110-
let Some(mac) = wait_nic_mac(&ip.device, cfg.timeout) else {
156+
let Some(mac) = wait_nic_mac(&ip.device, NIC_TIMEOUT) else {
111157
eprintln!(
112158
"sandbox-init: WARN: NIC {} not found, static config skipped",
113159
ip.device
@@ -142,28 +188,43 @@ fn mkdir_all(path: &str) -> Result<(), String> {
142188
fs::create_dir_all(path).map_err(|err| format!("mkdir {path}: {err}"))
143189
}
144190

145-
/// CH disks carry a virtio-blk serial; FC has no serials, so cocoon passes
146-
/// /dev/vdX paths there. 2ms polling — virtio probe completes a few ms into
147-
/// boot, so the first or second try normally hits; the old initramfs hook's
148-
/// 1s sleep granularity was a visible cost.
149-
fn resolve_disk(id: &str, timeout: Duration) -> Result<String, String> {
191+
/// Resolves every disk in one sysfs sweep per poll iteration instead of a
192+
/// scan per disk. CH disks carry a virtio-blk serial; FC has no serials, so
193+
/// cocoon passes /dev/vdX paths there. 2ms polling — virtio probe completes
194+
/// a few ms into boot, so the first or second try normally hits.
195+
fn resolve_disks(ids: &[String], timeout: Duration) -> Result<Vec<String>, String> {
150196
let deadline = Instant::now() + timeout;
197+
let mut found: Vec<Option<String>> = vec![None; ids.len()];
151198
loop {
152-
if let Some(dev) = try_resolve(id) {
153-
return Ok(dev);
199+
for (i, id) in ids.iter().enumerate() {
200+
if found[i].is_none() && id.starts_with("/dev/") && sys::is_block_dev(id) {
201+
found[i] = Some(id.clone());
202+
}
203+
}
204+
if found.iter().any(Option::is_none) {
205+
scan_serials(ids, &mut found);
206+
}
207+
if found.iter().all(Option::is_some) {
208+
return Ok(found.into_iter().flatten().collect());
154209
}
155210
if Instant::now() >= deadline {
156-
return Err(format!("disk {id} not found within {timeout:?}"));
211+
let missing: Vec<&str> = ids
212+
.iter()
213+
.zip(&found)
214+
.filter(|(_, f)| f.is_none())
215+
.map(|(id, _)| id.as_str())
216+
.collect();
217+
return Err(format!("disks not found within {timeout:?}: {missing:?}"));
157218
}
158219
std::thread::sleep(POLL_INTERVAL);
159220
}
160221
}
161222

162-
fn try_resolve(id: &str) -> Option<String> {
163-
if id.starts_with("/dev/") {
164-
return sys::is_block_dev(id).then(|| id.to_string());
165-
}
166-
for entry in fs::read_dir("/sys/block").ok()?.flatten() {
223+
fn scan_serials(ids: &[String], found: &mut [Option<String>]) {
224+
let Ok(entries) = fs::read_dir("/sys/block") else {
225+
return;
226+
};
227+
for entry in entries.flatten() {
167228
let Ok(name) = entry.file_name().into_string() else {
168229
continue;
169230
};
@@ -175,14 +236,18 @@ fn try_resolve(id: &str) -> Option<String> {
175236
format!("/sys/block/{name}/serial"),
176237
format!("/sys/block/{name}/device/serial"),
177238
];
178-
if paths
179-
.iter()
180-
.any(|p| fs::read_to_string(p).is_ok_and(|s| s.trim_end() == id))
181-
{
182-
return Some(format!("/dev/{name}"));
239+
for path in paths {
240+
let Ok(serial) = fs::read_to_string(&path) else {
241+
continue;
242+
};
243+
let serial = serial.trim_end();
244+
for (i, id) in ids.iter().enumerate() {
245+
if found[i].is_none() && id == serial {
246+
found[i] = Some(format!("/dev/{name}"));
247+
}
248+
}
183249
}
184250
}
185-
None
186251
}
187252

188253
fn uptime() -> String {

boot/init/src/cfg.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ pub struct BootCfg {
2626
pub init: String,
2727
/// Fatal errors drop to /bin/sh (debug initramfs) instead of poweroff.
2828
pub debug: bool,
29+
/// Emit one pre-handoff line with per-phase µs timings (sandbox.trace=1).
30+
pub trace: bool,
2931
}
3032

3133
/// One `ip=<addr>::<gw>:<mask>:<host>:<dev>:off[:dns0[:dns1]]` param
@@ -50,6 +52,7 @@ pub fn parse(cmdline: &str) -> Result<BootCfg, String> {
5052
ips: Vec::new(),
5153
init: DEFAULT_INIT.to_string(),
5254
debug: false,
55+
trace: false,
5356
};
5457
for tok in cmdline.split_ascii_whitespace() {
5558
let (key, val) = tok.split_once('=').unwrap_or((tok, ""));
@@ -70,6 +73,7 @@ pub fn parse(cmdline: &str) -> Result<BootCfg, String> {
7073
}
7174
"cocoon.hostname" if !val.is_empty() => cfg.hostname = Some(val.to_string()),
7275
"sandbox.debug" => cfg.debug = debug_token(val),
76+
"sandbox.trace" => cfg.trace = debug_token(val),
7377
"ip" => {
7478
if let Some(param) = parse_ip_param(val) {
7579
cfg.ips.push(param);
@@ -280,6 +284,15 @@ mod tests {
280284
assert!(!parse(&format!("{base} sandbox.debug=0")).unwrap().debug);
281285
}
282286

287+
#[test]
288+
fn parse_trace_forms() {
289+
let base = "cocoon.layers=l0 cocoon.cow=cow";
290+
assert!(!parse(base).unwrap().trace);
291+
assert!(parse(&format!("{base} sandbox.trace=1")).unwrap().trace);
292+
assert!(parse(&format!("{base} sandbox.trace")).unwrap().trace);
293+
assert!(!parse(&format!("{base} sandbox.trace=0")).unwrap().trace);
294+
}
295+
283296
#[test]
284297
fn parse_skips_empty_layer_entries() {
285298
let cfg = parse("cocoon.layers=a,,b, cocoon.cow=cow").unwrap();

0 commit comments

Comments
 (0)