Skip to main content

cobalt/
stalls.rs

1// Copyright 2025 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use anyhow::Result;
6use cobalt_client::traits::AsEventCode;
7use fidl_fuchsia_metrics as fmetrics;
8use futures::StreamExt;
9use memory_metrics_registry::cobalt_registry;
10use stalls::refaults::RefaultProvider;
11use stalls::{MemoryStallMetrics, StallProvider};
12use zx::MonotonicInstant;
13
14use crate::error_from_metrics_error;
15
16/// Collect and publish to Cobalt memory stall and page refault increase rates, every hour.
17pub async fn collect_stalls_forever(
18    stalls_provider: impl StallProvider,
19    refault_provider: impl RefaultProvider,
20    metric_event_logger: fmetrics::MetricEventLoggerProxy,
21) -> Result<()> {
22    let mut last_stall = MemoryStallMetrics::default();
23    let mut last_refaults = 0;
24
25    // Wait for one hour after device start to get the first stall value. We don't use the one-hour
26    // timer as we may have been started later than at boot exactly.
27    fuchsia_async::Timer::new(MonotonicInstant::ZERO + zx::Duration::from_hours(1)).await;
28
29    let mut timer = fuchsia_async::Interval::new(zx::Duration::from_hours(1));
30    loop {
31        let new_stall = stalls_provider.get_stall_info()?;
32        let new_refaults = refault_provider.get_count();
33
34        // The Cobalt metrics for stalls expect milliseconds, as defined in the Cobalt registry.
35        let stall_some_event = fmetrics::MetricEvent {
36            metric_id: cobalt_registry::MEMORY_STALLS_PER_HOUR_METRIC_ID,
37            payload: fmetrics::MetricEventPayload::IntegerValue(i64::try_from(
38                (new_stall.some - last_stall.some).as_millis(),
39            )?),
40            event_codes: vec![
41                cobalt_registry::MemoryMetricDimensionStallType::Some.as_event_code(),
42            ],
43        };
44        let stall_full_event = fmetrics::MetricEvent {
45            metric_id: cobalt_registry::MEMORY_STALLS_PER_HOUR_METRIC_ID,
46            payload: fmetrics::MetricEventPayload::IntegerValue(i64::try_from(
47                (new_stall.full - last_stall.full).as_millis(),
48            )?),
49            event_codes: vec![
50                cobalt_registry::MemoryMetricDimensionStallType::Full.as_event_code(),
51            ],
52        };
53        // The counter is monotonic, but let's saturate to 0 if it
54        // ever gets reset to avoid unsigned underflow.
55        let refaults_event = fmetrics::MetricEvent {
56            metric_id: cobalt_registry::MEMORY_PAGE_REFAULTS_PER_HOUR_METRIC_ID,
57            payload: fmetrics::MetricEventPayload::IntegerValue(
58                new_refaults.saturating_sub(last_refaults).try_into().unwrap_or(i64::MAX),
59            ),
60            event_codes: vec![],
61        };
62
63        last_stall = new_stall;
64        last_refaults = new_refaults;
65
66        let events = vec![stall_some_event, stall_full_event, refaults_event];
67        metric_event_logger.log_metric_events(&events).await?.map_err(error_from_metrics_error)?;
68        timer.next().await;
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75    use anyhow::anyhow;
76    use fuchsia_async as fasync;
77    use futures::task::Poll;
78    use std::sync::Arc;
79    use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
80    use std::time::Duration;
81
82    fn get_stall_provider() -> impl StallProvider {
83        #[derive(Clone)]
84        struct FakeStallProvider {
85            count: Arc<AtomicU32>,
86        }
87
88        impl Default for FakeStallProvider {
89            fn default() -> Self {
90                Self { count: Arc::new(AtomicU32::new(1)) }
91            }
92        }
93
94        impl StallProvider for FakeStallProvider {
95            fn get_stall_info(&self) -> Result<MemoryStallMetrics> {
96                let count = self.count.fetch_add(1, Ordering::Relaxed);
97                let memory_stall = MemoryStallMetrics {
98                    some: Duration::from_millis((count * 10).into()),
99                    full: Duration::from_millis((count * 20).into()),
100                };
101                Ok(memory_stall)
102            }
103        }
104
105        FakeStallProvider::default()
106    }
107
108    fn get_refault_provider() -> impl RefaultProvider {
109        #[derive(Clone, Default)]
110        struct FakeRefaultProvider {
111            calls: Arc<AtomicU64>,
112        }
113
114        impl RefaultProvider for FakeRefaultProvider {
115            fn get_count(&self) -> u64 {
116                // Return 100 at hour 1 (delta = 100) and 350 at hour 2 (delta = 250) so that
117                // the two intervals produce distinct deltas.
118                match self.calls.fetch_add(1, Ordering::Relaxed) {
119                    0 => 100,
120                    _ => 350,
121                }
122            }
123        }
124
125        FakeRefaultProvider::default()
126    }
127
128    #[test]
129    fn test_periodic_stalls_collection() -> Result<()> {
130        // Setup executor.
131        let mut exec = fasync::TestExecutor::new_with_fake_time();
132
133        // Setup mock data providers.
134        let data_provider = get_stall_provider();
135        let refault_provider = get_refault_provider();
136
137        // Setup test proxy to observe emitted events from the service.
138        let (metric_event_logger, metric_event_request_stream) =
139            fidl::endpoints::create_proxy_and_stream::<fmetrics::MetricEventLoggerMarker>();
140
141        // Set the time to shortly after boot
142        exec.set_fake_time(
143            (zx::MonotonicInstant::ZERO + zx::Duration::from_seconds(3 * 60)).into(),
144        );
145        // Service under test.
146        let mut stalls_collector = fuchsia_async::Task::spawn(async move {
147            collect_stalls_forever(data_provider, refault_provider, metric_event_logger).await
148        });
149
150        // Give the service the opportunity to run.
151        assert!(
152            exec.run_until_stalled(&mut stalls_collector).is_pending(),
153            "Stalls collection service returned unexpectedly early"
154        );
155
156        // Ensure no metrics has been uploaded yet.
157        let mut metric_event_request_future = metric_event_request_stream.into_future();
158        assert!(
159            exec.run_until_stalled(&mut metric_event_request_future).is_pending(),
160            "Stalls collection service returned unexpectedly early"
161        );
162
163        // Fake the passage of time, so that collect_metrics may do a capture.
164        assert!(
165            exec.run_until_stalled(&mut std::pin::pin!(fasync::TestExecutor::advance_to(
166                exec.now() + zx::Duration::from_seconds(60 * 60 + 10)
167            )))
168            .is_ready(),
169            "Failed to advance time"
170        );
171
172        // Ensure we have one and only one event ready for consumption.
173        let Poll::Ready((event, metric_event_request_stream)) =
174            exec.run_until_stalled(&mut metric_event_request_future)
175        else {
176            panic!("Failed to receive metrics")
177        };
178        let event = event.ok_or_else(|| anyhow!("Metrics stream unexpectedly closed"))??;
179        match event {
180            fmetrics::MetricEventLoggerRequest::LogMetricEvents { events, responder, .. } => {
181                assert_eq!(events.len(), 3);
182                // Kernel metrics
183                assert_eq!(
184                    events[0],
185                    fmetrics::MetricEvent {
186                        metric_id: cobalt_registry::MEMORY_STALLS_PER_HOUR_METRIC_ID,
187                        event_codes: vec![
188                            cobalt_registry::MemoryMetricDimensionStallType::Some.as_event_code()
189                        ],
190                        payload: fmetrics::MetricEventPayload::IntegerValue(10)
191                    }
192                );
193                assert_eq!(
194                    events[1],
195                    fmetrics::MetricEvent {
196                        metric_id: cobalt_registry::MEMORY_STALLS_PER_HOUR_METRIC_ID,
197                        event_codes: vec![
198                            cobalt_registry::MemoryMetricDimensionStallType::Full.as_event_code()
199                        ],
200                        payload: fmetrics::MetricEventPayload::IntegerValue(20)
201                    }
202                );
203                assert_eq!(
204                    events[2],
205                    fmetrics::MetricEvent {
206                        metric_id: cobalt_registry::MEMORY_PAGE_REFAULTS_PER_HOUR_METRIC_ID,
207                        event_codes: vec![],
208                        payload: fmetrics::MetricEventPayload::IntegerValue(100)
209                    }
210                );
211                responder.send(Ok(()))?;
212            }
213            _ => panic!("Unexpected metric event"),
214        }
215
216        let mut metric_event_request_future = metric_event_request_stream.into_future();
217
218        assert!(exec.run_until_stalled(&mut metric_event_request_future).is_pending());
219
220        // Advance to the next hour
221        assert!(
222            exec.run_until_stalled(&mut std::pin::pin!(fasync::TestExecutor::advance_to(
223                (zx::MonotonicInstant::ZERO + zx::Duration::from_seconds(60 * 60 * 2 + 10)).into()
224            )))
225            .is_ready(),
226            "Failed to advance time"
227        );
228
229        // Ensure we have one and only one event ready for consumption.
230        let Poll::Ready((event, metric_event_request_stream)) =
231            exec.run_until_stalled(&mut metric_event_request_future)
232        else {
233            panic!("Failed to receive metrics")
234        };
235        let event = event.ok_or_else(|| anyhow!("Metrics stream unexpectedly closed"))??;
236        match event {
237            fmetrics::MetricEventLoggerRequest::LogMetricEvents { events, responder, .. } => {
238                assert_eq!(events.len(), 3);
239                // Kernel metrics
240                assert_eq!(
241                    events[0],
242                    fmetrics::MetricEvent {
243                        metric_id: cobalt_registry::MEMORY_STALLS_PER_HOUR_METRIC_ID,
244                        event_codes: vec![
245                            cobalt_registry::MemoryMetricDimensionStallType::Some.as_event_code()
246                        ],
247                        payload: fmetrics::MetricEventPayload::IntegerValue(10)
248                    }
249                );
250                assert_eq!(
251                    events[1],
252                    fmetrics::MetricEvent {
253                        metric_id: cobalt_registry::MEMORY_STALLS_PER_HOUR_METRIC_ID,
254                        event_codes: vec![
255                            cobalt_registry::MemoryMetricDimensionStallType::Full.as_event_code()
256                        ],
257                        payload: fmetrics::MetricEventPayload::IntegerValue(20)
258                    }
259                );
260                assert_eq!(
261                    events[2],
262                    fmetrics::MetricEvent {
263                        metric_id: cobalt_registry::MEMORY_PAGE_REFAULTS_PER_HOUR_METRIC_ID,
264                        event_codes: vec![],
265                        payload: fmetrics::MetricEventPayload::IntegerValue(250)
266                    }
267                );
268                responder.send(Ok(()))?;
269            }
270            _ => panic!("Unexpected metric event"),
271        }
272
273        assert!(
274            exec.run_until_stalled(&mut metric_event_request_stream.into_future()).is_pending()
275        );
276
277        Ok(())
278    }
279}