1use std::array::TryFromSliceError;
7use std::collections::HashMap;
8use std::io::ErrorKind;
9use std::num::TryFromIntError;
10use std::path::{Path, PathBuf};
11use std::sync::Arc;
12
13use anyhow::{anyhow, Result};
14use tokio::sync::RwLock;
15use tracing::debug;
16use zbus::zvariant::ObjectPath;
17
18use crate::config::Config;
19use crate::dbus::zbus_systemd::ManagerProxy;
20use crate::dbus::zbus_unit::UnitProxy;
21use crate::MachineStats;
22
23pub type BootBlameStats = HashMap<String, f64>;
25
26const BOOT_ID_PATH: &str = "/proc/sys/kernel/random/boot_id";
27const BOOT_BLAME_CACHE_SUFFIX: &str = "boot_blame.bin";
28
29type BootCacheResult<T> = std::result::Result<T, BootCacheError>;
30
31#[derive(Debug, thiserror::Error)]
32enum BootCacheError {
33 #[error("boot cache I/O error: {0}")]
34 Io(#[from] std::io::Error),
35 #[error("boot id from {BOOT_ID_PATH} was empty")]
36 EmptyBootId,
37 #[error("boot cache payload decode error: {0}")]
38 InvalidPayload(&'static str),
39 #[error("boot cache UTF-8 decode error: {0}")]
40 Utf8(#[from] std::string::FromUtf8Error),
41 #[error("boot cache integer conversion error: {0}")]
42 IntConversion(#[from] TryFromIntError),
43 #[error("boot cache slice conversion error: {0}")]
44 SliceConversion(#[from] TryFromSliceError),
45}
46
47fn cache_file_path(cache_dir: &Path, boot_id: &str) -> PathBuf {
48 cache_dir.join(format!("{boot_id}.{BOOT_BLAME_CACHE_SUFFIX}"))
49}
50
51async fn get_boot_id() -> BootCacheResult<String> {
52 let boot_id = tokio::fs::read_to_string(BOOT_ID_PATH).await?;
53 let boot_id = boot_id.trim().to_string();
54 if boot_id.is_empty() {
55 return Err(BootCacheError::EmptyBootId);
56 }
57 Ok(boot_id)
58}
59
60fn encode_boot_blame_stats(stats: &BootBlameStats) -> BootCacheResult<Vec<u8>> {
61 let mut out = Vec::new();
62 let entry_count = u32::try_from(stats.len())?;
63 out.extend_from_slice(&entry_count.to_le_bytes());
64
65 for (unit_name, activation_time) in stats {
66 let unit_name_bytes = unit_name.as_bytes();
67 let unit_name_len = u32::try_from(unit_name_bytes.len())?;
68 out.extend_from_slice(&unit_name_len.to_le_bytes());
69 out.extend_from_slice(unit_name_bytes);
70 out.extend_from_slice(&activation_time.to_le_bytes());
71 }
72
73 Ok(out)
74}
75
76struct DecodedBootBlame {
83 stats: BootBlameStats,
84 transport: Option<crate::CollectorTransport>,
85}
86
87fn decode_boot_blame_stats(content: &[u8]) -> BootCacheResult<BootBlameStats> {
88 const U32_BYTES: usize = std::mem::size_of::<u32>();
89 const F64_BYTES: usize = std::mem::size_of::<f64>();
90 fn read_u32(bytes: &[u8], offset: &mut usize) -> BootCacheResult<u32> {
91 if *offset + std::mem::size_of::<u32>() > bytes.len() {
92 return Err(BootCacheError::InvalidPayload("unexpected end of payload"));
93 }
94 let value =
95 u32::from_le_bytes(bytes[*offset..*offset + std::mem::size_of::<u32>()].try_into()?);
96 *offset += std::mem::size_of::<u32>();
97 Ok(value)
98 }
99
100 if content.len() < U32_BYTES {
101 return Err(BootCacheError::InvalidPayload("payload too small"));
102 }
103
104 let mut offset = 0usize;
105 let entry_count = read_u32(content, &mut offset)? as usize;
106 let mut stats = BootBlameStats::with_capacity(entry_count);
107
108 for _ in 0..entry_count {
109 let name_len = read_u32(content, &mut offset)? as usize;
110 if offset + name_len + F64_BYTES > content.len() {
111 return Err(BootCacheError::InvalidPayload("invalid payload size"));
112 }
113 let unit_name = String::from_utf8(content[offset..offset + name_len].to_vec())?;
114 offset += name_len;
115 let activation_time = f64::from_le_bytes(content[offset..offset + F64_BYTES].try_into()?);
116 offset += F64_BYTES;
117 stats.insert(unit_name, activation_time);
118 }
119
120 if offset != content.len() {
121 return Err(BootCacheError::InvalidPayload("trailing bytes in payload"));
122 }
123
124 Ok(stats)
125}
126
127fn encode_cached_boot_blame(
128 stats: &BootBlameStats,
129 transport: crate::CollectorTransport,
130) -> BootCacheResult<Vec<u8>> {
131 let mut out = encode_boot_blame_stats(stats)?;
132 out.push(transport as u8);
133 Ok(out)
134}
135
136fn decode_cached_boot_blame(content: &[u8]) -> BootCacheResult<DecodedBootBlame> {
137 if let Some((payload, [marker])) = content.split_at_checked(content.len().saturating_sub(1)) {
142 if *marker <= 1 {
143 if let Ok(stats) = decode_boot_blame_stats(payload) {
144 let transport = if *marker == 1 {
145 crate::CollectorTransport::Varlink
146 } else {
147 crate::CollectorTransport::Dbus
148 };
149 return Ok(DecodedBootBlame {
150 stats,
151 transport: Some(transport),
152 });
153 }
154 }
155 }
156 Ok(DecodedBootBlame {
157 stats: decode_boot_blame_stats(content)?,
158 transport: None,
159 })
160}
161
162async fn read_cached_boot_blame_from_dir(
163 cache_dir: &Path,
164 boot_id: &str,
165) -> BootCacheResult<Option<DecodedBootBlame>> {
166 let cache_path = cache_file_path(cache_dir, boot_id);
167 let content = match tokio::fs::read(&cache_path).await {
168 Ok(content) => content,
169 Err(err) if err.kind() == ErrorKind::NotFound => return Ok(None),
170 Err(err) => return Err(err.into()),
171 };
172 Ok(Some(decode_cached_boot_blame(&content)?))
173}
174
175async fn write_cached_boot_blame_to_dir(
176 cache_dir: &Path,
177 boot_id: &str,
178 stats: &BootBlameStats,
179 transport: crate::CollectorTransport,
180) -> BootCacheResult<()> {
181 tokio::fs::create_dir_all(cache_dir).await?;
182 let cache_path = cache_file_path(cache_dir, boot_id);
183 let encoded = encode_cached_boot_blame(stats, transport)?;
184 tokio::fs::write(cache_path, encoded).await?;
185 Ok(())
186}
187
188async fn get_unit_activation_time(
191 connection: &zbus::Connection,
192 unit_path: &ObjectPath<'_>,
193) -> Result<f64> {
194 let unit_proxy = UnitProxy::builder(connection)
195 .cache_properties(zbus::proxy::CacheProperties::No)
196 .path(unit_path)?
197 .build()
198 .await?;
199
200 let inactive_exit = unit_proxy.inactive_exit_timestamp().await?;
201 let active_enter = unit_proxy.active_enter_timestamp().await?;
202
203 if inactive_exit == 0 || active_enter == 0 {
205 return Ok(0.0);
206 }
207
208 let activation_time_usec = active_enter.saturating_sub(inactive_exit);
210 let activation_time_sec = activation_time_usec as f64 / 1_000_000.0;
211
212 Ok(activation_time_sec)
213}
214
215async fn collect_boot_blame_dbus(
218 config: &Config,
219 connection: &zbus::Connection,
220) -> Result<BootBlameStats> {
221 let systemd_proxy = ManagerProxy::builder(connection)
222 .cache_properties(zbus::proxy::CacheProperties::No)
223 .build()
224 .await?;
225 let units = systemd_proxy.list_units().await?;
226
227 let mut unit_times: Vec<(String, f64)> = Vec::new();
228
229 for unit_info in units {
231 let unit_name = unit_info.0;
232 let unit_path = unit_info.6;
233
234 if config.boot_blame.blocklist.contains(&unit_name) {
236 debug!("Skipping boot blame for {} due to blocklist", &unit_name);
237 continue;
238 }
239 if !config.boot_blame.allowlist.is_empty()
241 && !config.boot_blame.allowlist.contains(&unit_name)
242 {
243 continue;
244 }
245
246 match get_unit_activation_time(connection, &unit_path).await {
247 Ok(time) if time > 0.0 => {
248 unit_times.push((unit_name, time));
249 }
250 Ok(_) => {
251 }
253 Err(e) => {
254 debug!("Failed to get activation time for {}: {}", unit_name, e);
255 }
256 }
257 }
258
259 unit_times.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
261
262 let num_slowest = config.boot_blame.num_slowest_units as usize;
264 unit_times.truncate(num_slowest);
265
266 Ok(unit_times.into_iter().collect())
268}
269
270pub async fn update_boot_blame_stats(
276 config: Arc<Config>,
277 dbus: crate::DbusCell,
278 dbus_timeout: u64,
279 machine_stats: Arc<RwLock<MachineStats>>,
280 no_fallback: bool,
281) -> Result<()> {
282 debug!("Starting boot blame stats collection");
283
284 let mut maybe_boot_id = None;
285 if config.boot_blame.cache_enabled {
286 let cached_stats = machine_stats.read().await.boot_blame.clone();
289 if cached_stats.is_some() {
290 debug!("Using in-memory cached boot blame stats");
291 return Ok(());
292 }
293
294 let cache_dir = Path::new(&config.boot_blame.cache_dir);
295 match get_boot_id().await {
296 Ok(boot_id) => {
297 match read_cached_boot_blame_from_dir(cache_dir, &boot_id).await {
298 Ok(Some(cached)) => {
299 let cache_path = cache_file_path(cache_dir, &boot_id);
300 debug!(
301 "Using cached boot blame stats from {}",
302 cache_path.display()
303 );
304 let mut stats = machine_stats.write().await;
305 stats.boot_blame = Some(cached.stats);
306 match cached.transport {
307 Some(transport) => {
308 stats.varlink_usage.boot_blame = Some(transport);
312 }
313 None => {
314 debug!(
318 "Boot blame cache predates transport tracking, re-collecting"
319 );
320 drop(stats);
321 maybe_boot_id = Some(boot_id);
322 return collect_and_cache(
323 &config,
324 &dbus,
325 dbus_timeout,
326 no_fallback,
327 machine_stats,
328 maybe_boot_id,
329 )
330 .await;
331 }
332 }
333 return Ok(());
334 }
335 Ok(None) => {
336 debug!("No cached boot blame stats found for boot id {}", boot_id);
337 }
338 Err(err) => {
339 debug!(
340 "Failed to load boot blame cache for boot id {}: {}",
341 boot_id, err
342 );
343 }
344 }
345 maybe_boot_id = Some(boot_id);
346 }
347 Err(err) => {
348 debug!("Failed to retrieve boot id for boot blame cache: {}", err);
349 }
350 }
351 }
352
353 collect_and_cache(
354 &config,
355 &dbus,
356 dbus_timeout,
357 no_fallback,
358 machine_stats,
359 maybe_boot_id,
360 )
361 .await
362}
363
364async fn collect_and_cache(
368 config: &Arc<Config>,
369 dbus: &crate::DbusCell,
370 dbus_timeout: u64,
371 no_fallback: bool,
372 machine_stats: Arc<RwLock<MachineStats>>,
373 maybe_boot_id: Option<String>,
374) -> Result<()> {
375 let use_varlink = config.use_varlink(&[config.boot_blame.varlink]);
376 let (boot_blame_stats, transport) = if use_varlink {
377 match crate::varlink_boot::get_boot_blame_stats(
378 crate::varlink_boot::METRICS_SOCKET_PATH,
379 &config.boot_blame,
380 )
381 .await
382 {
383 Ok(stats) => (stats, crate::CollectorTransport::Varlink),
384 Err(err) => {
385 crate::varlink_fallback::report_varlink_failure(
388 no_fallback,
389 "boot blame",
390 "D-Bus",
391 err,
392 )?;
393 let connection = crate::dbus_connection(dbus, dbus_timeout)
394 .await
395 .map_err(|e| anyhow!("D-Bus connection error: {:?}", e))?;
396 (
397 collect_boot_blame_dbus(config, &connection).await?,
398 crate::CollectorTransport::Dbus,
399 )
400 }
401 }
402 } else {
403 let connection = crate::dbus_connection(dbus, dbus_timeout)
404 .await
405 .map_err(|e| anyhow!("D-Bus connection error: {:?}", e))?;
406 (
407 collect_boot_blame_dbus(config, &connection).await?,
408 crate::CollectorTransport::Dbus,
409 )
410 };
411
412 debug!("Collected {} boot blame stats", boot_blame_stats.len());
413
414 let mut stats = machine_stats.write().await;
416 stats.boot_blame = Some(boot_blame_stats);
417 stats.varlink_usage.boot_blame = Some(transport);
418 if config.boot_blame.cache_enabled {
419 if let Some(boot_id) = maybe_boot_id {
420 if let Some(cached_stats) = stats.boot_blame.as_ref() {
421 let cache_dir = Path::new(&config.boot_blame.cache_dir);
422 if let Err(err) =
423 write_cached_boot_blame_to_dir(cache_dir, &boot_id, cached_stats, transport)
424 .await
425 {
426 debug!(
427 "Failed to write boot blame cache for boot id {} to {}: {}",
428 boot_id, config.boot_blame.cache_dir, err
429 );
430 } else {
431 debug!("Updated boot blame cache for boot id {}", boot_id);
432 }
433 }
434 }
435 }
436
437 Ok(())
438}
439
440#[cfg(test)]
441mod tests {
442 use super::*;
443
444 #[test]
445 fn test_boot_blame_cache_encode_decode_roundtrip() {
446 let mut stats = BootBlameStats::new();
447 stats.insert("foo.service".to_string(), 12.3);
448 stats.insert("bar.service".to_string(), 45.6);
449
450 let encoded = encode_boot_blame_stats(&stats).expect("encode should succeed");
451 let decoded = decode_boot_blame_stats(&encoded).expect("decode should succeed");
452 assert_eq!(stats, decoded);
453 }
454
455 #[test]
456 fn test_boot_blame_cache_decode_invalid_payload() {
457 let invalid_payload = vec![0, 1, 2];
458 assert!(decode_boot_blame_stats(&invalid_payload).is_err());
459 }
460
461 #[test]
462 fn test_cached_boot_blame_roundtrips_transport() {
463 let mut stats = BootBlameStats::new();
466 stats.insert("foo.service".to_string(), 12.3);
467 for transport in [
468 crate::CollectorTransport::Varlink,
469 crate::CollectorTransport::Dbus,
470 ] {
471 let encoded = encode_cached_boot_blame(&stats, transport).expect("encode");
472 let decoded = decode_cached_boot_blame(&encoded).expect("decode");
473 assert_eq!(stats, decoded.stats);
474 assert_eq!(Some(transport), decoded.transport);
475 }
476 }
477
478 #[test]
479 fn test_legacy_boot_blame_cache_decodes_without_transport() {
480 let mut stats = BootBlameStats::new();
483 stats.insert("foo.service".to_string(), 12.3);
484 stats.insert("bar.service".to_string(), 45.6);
485 let encoded = encode_boot_blame_stats(&stats).expect("encode should succeed");
486 let decoded = decode_cached_boot_blame(&encoded).expect("decode should succeed");
487 assert_eq!(stats, decoded.stats);
488 assert_eq!(None, decoded.transport);
489 }
490
491 #[tokio::test]
492 async fn test_boot_blame_cache_read_write_roundtrip() {
493 let temp_dir = tempfile::tempdir().expect("create temp dir");
494 let boot_id = "00000000-0000-0000-0000-000000000001";
495 let mut stats = BootBlameStats::new();
496 stats.insert("foo.service".to_string(), 1.25);
497
498 write_cached_boot_blame_to_dir(
499 temp_dir.path(),
500 boot_id,
501 &stats,
502 crate::CollectorTransport::Varlink,
503 )
504 .await
505 .expect("write cache");
506 let read = read_cached_boot_blame_from_dir(temp_dir.path(), boot_id)
507 .await
508 .expect("read cache")
509 .expect("cache hit");
510 assert_eq!(stats, read.stats);
511 assert_eq!(Some(crate::CollectorTransport::Varlink), read.transport);
512 }
513
514 #[tokio::test]
515 async fn test_boot_blame_cache_read_missing_file() {
516 let temp_dir = tempfile::tempdir().expect("create temp dir");
517 let missing = read_cached_boot_blame_from_dir(
518 temp_dir.path(),
519 "00000000-0000-0000-0000-000000000002",
520 )
521 .await
522 .expect("missing cache should not error");
523 assert!(missing.is_none());
524 }
525}