1use struct_field_names_as_array::FieldNamesAsArray;
7use thiserror::Error;
8use tracing::error;
9
10#[derive(Error, Debug)]
11pub enum MonitordTimerError {
12 #[error("Timer D-Bus error: {0}")]
13 ZbusError(#[from] zbus::Error),
14}
15
16#[derive(
17 serde::Serialize, serde::Deserialize, Clone, Debug, Default, Eq, FieldNamesAsArray, PartialEq,
18)]
19
20pub struct TimerStats {
23 pub accuracy_usec: u64,
25 pub fixed_random_delay: bool,
27 pub last_trigger_usec: u64,
29 pub last_trigger_usec_monotonic: u64,
31 pub next_elapse_usec_monotonic: u64,
33 pub next_elapse_usec_realtime: u64,
35 pub persistent: bool,
37 pub randomized_delay_usec: u64,
39 pub remain_after_elapse: bool,
41 pub service_unit_last_state_change_usec: u64,
43 pub service_unit_last_state_change_usec_monotonic: u64,
45}
46
47pub const TIMER_STATS_FIELD_NAMES: &[&str] = &TimerStats::FIELD_NAMES_AS_ARRAY;
48
49#[tracing::instrument(level = "debug", skip(connection))]
50pub async fn collect_timer_stats(
51 connection: &zbus::Connection,
52 unit: &crate::units::ListedUnit,
53) -> Result<TimerStats, MonitordTimerError> {
54 let mut timer_stats = TimerStats::default();
55
56 let pt = crate::dbus::zbus_timer::TimerProxy::builder(connection)
57 .cache_properties(zbus::proxy::CacheProperties::No)
58 .path(unit.unit_object_path.clone())?
59 .build()
60 .await?;
61 let service_unit = pt.unit().await?;
64 let mut service_unit_last_state_change_usec: Result<u64, zbus::Error> = Ok(0);
65 let mut service_unit_last_state_change_usec_monotonic: Result<u64, zbus::Error> = Ok(0);
66 if service_unit.is_empty() {
67 error!("{}: No service unit name found for timer.", unit.name);
68 } else {
69 let mp = crate::dbus::zbus_systemd::ManagerProxy::builder(connection)
71 .cache_properties(zbus::proxy::CacheProperties::No)
72 .build()
73 .await?;
74 let service_unit_path = mp.get_unit(&service_unit).await?;
75 let up = crate::dbus::zbus_unit::UnitProxy::builder(connection)
77 .cache_properties(zbus::proxy::CacheProperties::No)
78 .path(service_unit_path)?
79 .build()
80 .await?;
81
82 (
83 service_unit_last_state_change_usec,
84 service_unit_last_state_change_usec_monotonic,
85 ) = tokio::join!(
86 up.state_change_timestamp(),
87 up.state_change_timestamp_monotonic(),
88 );
89 }
90 timer_stats.service_unit_last_state_change_usec = service_unit_last_state_change_usec?;
91 timer_stats.service_unit_last_state_change_usec_monotonic =
92 service_unit_last_state_change_usec_monotonic?;
93
94 let (
97 accuracy_usec,
98 fixed_random_delay,
99 last_trigger_usec,
100 last_trigger_usec_monotonic,
101 persistent,
102 next_elapse_usec_monotonic,
103 next_elapse_usec_realtime,
104 randomized_delay_usec,
105 remain_after_elapse,
106 ) = tokio::join!(
107 pt.accuracy_usec(),
108 pt.fixed_random_delay(),
109 pt.last_trigger_usec(),
110 pt.last_trigger_usec_monotonic(),
111 pt.persistent(),
112 pt.next_elapse_usec_monotonic(),
113 pt.next_elapse_usec_realtime(),
114 pt.randomized_delay_usec(),
115 pt.remain_after_elapse(),
116 );
117
118 timer_stats.accuracy_usec = accuracy_usec?;
119 timer_stats.fixed_random_delay = fixed_random_delay?;
120 timer_stats.last_trigger_usec = last_trigger_usec?;
121 timer_stats.last_trigger_usec_monotonic = last_trigger_usec_monotonic?;
122 timer_stats.persistent = persistent?;
123 timer_stats.next_elapse_usec_monotonic = next_elapse_usec_monotonic?;
124 timer_stats.next_elapse_usec_realtime = next_elapse_usec_realtime?;
125 timer_stats.randomized_delay_usec = randomized_delay_usec?;
126 timer_stats.remain_after_elapse = remain_after_elapse?;
127
128 Ok(timer_stats)
129}
130
131pub fn merge_timer_stats(
142 target: &mut crate::units::SystemdUnitStats,
143 collected: crate::units::SystemdUnitStats,
144 elapsed_ms: f64,
145) {
146 record_backfill_duration(target, elapsed_ms);
147 target.collection_timings.timer_dbus_fetches += collected.timer_stats.len() as u64;
148 target.timer_stats = collected.timer_stats;
149 target.timer_persistent_units = collected.timer_persistent_units;
150 target.timer_remain_after_elapse = collected.timer_remain_after_elapse;
151}
152
153pub fn record_backfill_duration(target: &mut crate::units::SystemdUnitStats, elapsed_ms: f64) {
160 target.collection_timings.per_unit_loop_ms += elapsed_ms;
161}
162
163pub async fn collect_all_timers_dbus(
169 connection: &zbus::Connection,
170 config: &crate::config::Config,
171) -> anyhow::Result<crate::units::SystemdUnitStats> {
172 use std::collections::HashMap;
173 use tracing::debug;
174
175 if !config.timers.enabled {
176 return Ok(crate::units::SystemdUnitStats::default());
177 }
178
179 let p = crate::dbus::zbus_systemd::ManagerProxy::builder(connection)
180 .cache_properties(zbus::proxy::CacheProperties::No)
181 .build()
182 .await?;
183 let units = p.list_units().await?;
184
185 let mut stats = crate::units::SystemdUnitStats::default();
186 let mut timer_stats_map = HashMap::new();
187
188 for unit_raw in units {
189 let unit: crate::units::ListedUnit = unit_raw.into();
190 if !unit
191 .name
192 .ends_with(crate::unit_constants::SYSTEMD_TIMER_SUFFIX)
193 {
194 continue;
195 }
196 if config.timers.blocklist.contains(&unit.name) {
197 debug!("Skipping timer stats for {} due to blocklist", &unit.name);
198 continue;
199 }
200 if !config.timers.allowlist.is_empty() && !config.timers.allowlist.contains(&unit.name) {
201 continue;
202 }
203 match collect_timer_stats(connection, &unit).await {
204 Ok(ts) => {
205 if ts.persistent {
206 stats.timer_persistent_units += 1;
207 }
208 if ts.remain_after_elapse {
209 stats.timer_remain_after_elapse += 1;
210 }
211 timer_stats_map.insert(unit.name.clone(), ts);
212 }
213 Err(err) => {
214 error!("Failed to get {} stats: {:#?}", &unit.name, err);
215 }
216 }
217 }
218
219 stats.timer_stats = timer_stats_map;
220 Ok(stats)
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226
227 #[test]
228 fn test_merge_timer_stats() {
229 let mut target = crate::units::SystemdUnitStats::default();
230 let mut collected = crate::units::SystemdUnitStats::default();
231 collected.timer_stats.insert(
232 "foo.timer".to_string(),
233 TimerStats {
234 persistent: true,
235 ..Default::default()
236 },
237 );
238 collected.timer_persistent_units = 1;
239 collected.timer_remain_after_elapse = 2;
240
241 merge_timer_stats(&mut target, collected, 12.5);
242
243 assert_eq!(target.timer_stats.len(), 1);
244 assert!(
245 target
246 .timer_stats
247 .get("foo.timer")
248 .expect("foo.timer should be merged")
249 .persistent
250 );
251 assert_eq!(target.timer_persistent_units, 1);
252 assert_eq!(target.timer_remain_after_elapse, 2);
253 assert_eq!(target.total_units, 0);
255 }
256
257 #[test]
258 fn test_merge_timer_stats_accounts_the_backfill() {
259 let mut target = crate::units::SystemdUnitStats::default();
262 target.collection_timings.per_unit_loop_ms = 2.5;
263 let mut collected = crate::units::SystemdUnitStats::default();
264 for name in ["foo.timer", "bar.timer"] {
265 collected
266 .timer_stats
267 .insert(name.to_string(), TimerStats::default());
268 }
269
270 merge_timer_stats(&mut target, collected, 10.0);
271
272 assert_eq!(target.collection_timings.per_unit_loop_ms, 12.5);
273 assert_eq!(target.collection_timings.timer_dbus_fetches, 2);
274 }
275
276 #[test]
277 fn test_record_backfill_duration_without_stats() {
278 let mut target = crate::units::SystemdUnitStats::default();
281 target.collection_timings.per_unit_loop_ms = 1.5;
282
283 record_backfill_duration(&mut target, 5000.0);
284
285 assert_eq!(target.collection_timings.per_unit_loop_ms, 5001.5);
286 assert_eq!(target.collection_timings.timer_dbus_fetches, 0);
287 assert!(target.timer_stats.is_empty());
288 }
289
290 #[test]
291 fn test_merge_timer_stats_with_no_timers() {
292 let mut target = crate::units::SystemdUnitStats::default();
295
296 merge_timer_stats(&mut target, crate::units::SystemdUnitStats::default(), 3.0);
297
298 assert_eq!(target.collection_timings.per_unit_loop_ms, 3.0);
299 assert_eq!(target.collection_timings.timer_dbus_fetches, 0);
300 }
301
302 #[test]
303 fn test_only_timer_units_are_selected() {
304 let selects = |name: &str| name.ends_with(crate::unit_constants::SYSTEMD_TIMER_SUFFIX);
309
310 assert!(selects("logrotate.timer"));
311 assert!(selects("systemd-tmpfiles-clean.timer"));
312
313 assert!(!selects("my.timer.service"));
314 assert!(!selects("foo.timers.service"));
315 assert!(!selects("timer.target"));
316 assert!(!selects("dbus-broker.service"));
317 }
318}