Skip to main content

kunobi_daemon/
warmup.rs

1//! Prefault a client shim so the next exec of the same path is not cold.
2//!
3//! The daemon cannot keep a pre-spawned shim: stdin and stdout belong to the
4//! process that launched the client. What it can do is fault the file into the
5//! OS page cache, and (on macOS) populate the per-path first-exec cache by
6//! spawning a child that exits immediately.
7//!
8//! Call [`warm_executable`] at daemon start and after a replacement commit.
9//! [`warm_spawn`] is for an argv the child understands as "exit without
10//! talking to the daemon" (for example `--warmup`). Neither function runs on a
11//! timer: a periodic exec fights idle-wakeup budgets, and the expensive miss is
12//! a new path after replace, not eviction of a few hundred kilobytes.
13
14use std::ffi::OsStr;
15use std::fs::File;
16use std::io::{self, Read};
17use std::path::Path;
18use std::process::{Command, Stdio};
19use std::time::{Duration, Instant};
20
21/// Bytes read per iteration. Matches a typical file-system block multiple and
22/// the transport window; the buffer is reused so RSS stays at this size.
23const WINDOW: usize = 65_536;
24
25/// How long [`warm_spawn`] waits before killing the child.
26pub const SPAWN_BUDGET: Duration = Duration::from_secs(2);
27
28/// Read `path` into the OS file cache without executing it.
29///
30/// The file length is taken from metadata so special files with no end (such as
31/// `/dev/zero`) cannot hang the caller. Missing, unreadable or directory paths
32/// return an error. Replacement treats those errors as non-fatal.
33pub fn warm_executable(path: &Path) -> io::Result<()> {
34    let mut file = File::open(path)?;
35    let metadata = file.metadata()?;
36    if metadata.is_dir() {
37        return Err(io::Error::new(
38            io::ErrorKind::IsADirectory,
39            "warmup path is a directory",
40        ));
41    }
42    let mut remaining = metadata.len();
43    let mut buffer = vec![0u8; WINDOW];
44    while remaining > 0 {
45        let want = WINDOW.min(remaining as usize);
46        let got = file.read(&mut buffer[..want])?;
47        if got == 0 {
48            break;
49        }
50        remaining -= got as u64;
51    }
52    Ok(())
53}
54
55/// Spawn `path` with `args`, wait for a successful exit, then return.
56///
57/// Stdio is detached. The child must exit without connecting to a daemon or
58/// taking its locks. Uses [`SPAWN_BUDGET`].
59pub fn warm_spawn(path: &Path, args: &[impl AsRef<OsStr>]) -> io::Result<()> {
60    warm_spawn_until(path, args, Instant::now() + SPAWN_BUDGET)
61}
62
63/// As [`warm_spawn`], with an explicit deadline.
64pub fn warm_spawn_until(
65    path: &Path,
66    args: &[impl AsRef<OsStr>],
67    deadline: Instant,
68) -> io::Result<()> {
69    let mut child = Command::new(path)
70        .args(args)
71        .stdin(Stdio::null())
72        .stdout(Stdio::null())
73        .stderr(Stdio::null())
74        .spawn()?;
75    loop {
76        match child.try_wait()? {
77            Some(status) if status.success() => return Ok(()),
78            Some(status) => {
79                return Err(io::Error::other(format!(
80                    "warmup {} exited {status}",
81                    path.display()
82                )));
83            }
84            None if Instant::now() >= deadline => {
85                let _ = child.kill();
86                let _ = child.wait();
87                return Err(io::Error::new(
88                    io::ErrorKind::TimedOut,
89                    format!("warmup {} exceeded its budget", path.display()),
90                ));
91            }
92            None => {
93                let slice = Duration::from_millis(1)
94                    .min(deadline.saturating_duration_since(Instant::now()));
95                if slice.is_zero() {
96                    continue;
97                }
98                std::thread::sleep(slice);
99            }
100        }
101    }
102}
103
104pub(crate) fn prefault_all(paths: impl IntoIterator<Item = impl AsRef<Path>>) {
105    for path in paths {
106        let _ = warm_executable(path.as_ref());
107    }
108}