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::PageFaultProvider;
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    page_fault_provider: impl PageFaultProvider,
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 = page_fault_provider.get_refault_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_page_fault_provider() -> impl PageFaultProvider {
109        #[derive(Clone, Default)]
110        struct FakePageFaultProvider {
111            calls: Arc<AtomicU64>,
112        }
113
114        impl PageFaultProvider for FakePageFaultProvider {
115            fn get_refault_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            fn get_fault_count(&self) -> u64 {
125                0
126            }
127        }
128
129        FakePageFaultProvider::default()
130    }
131
132    #[test]
133    fn test_periodic_stalls_collection() -> Result<()> {
134        // Setup executor.
135        let mut exec = fasync::TestExecutor::new_with_fake_time();
136
137        // Setup mock data providers.
138        let data_provider = get_stall_provider();
139        let page_fault_provider = get_page_fault_provider();
140
141        // Setup test proxy to observe emitted events from the service.
142        let (metric_event_logger, metric_event_request_stream) =
143            fidl::endpoints::create_proxy_and_stream::<fmetrics::MetricEventLoggerMarker>();
144
145        // Set the time to shortly after boot
146        exec.set_fake_time(
147            (zx::MonotonicInstant::ZERO + zx::Duration::from_seconds(3 * 60)).into(),
148        );
149        // Service under test.
150        let mut stalls_collector = fuchsia_async::Task::spawn(async move {
151            collect_stalls_forever(data_provider, page_fault_provider, metric_event_logger).await
152        });
153
154        // Give the service the opportunity to run.
155        assert!(
156            exec.run_until_stalled(&mut stalls_collector).is_pending(),
157            "Stalls collection service returned unexpectedly early"
158        );
159
160        // Ensure no metrics has been uploaded yet.
161        let mut metric_event_request_future = metric_event_request_stream.into_future();
162        assert!(
163            exec.run_until_stalled(&mut metric_event_request_future).is_pending(),
164            "Stalls collection service returned unexpectedly early"
165        );
166
167        // Fake the passage of time, so that collect_metrics may do a capture.
168        assert!(
169            exec.run_until_stalled(&mut std::pin::pin!(fasync::TestExecutor::advance_to(
170                exec.now() + zx::Duration::from_seconds(60 * 60 + 10)
171            )))
172            .is_ready(),
173            "Failed to advance time"
174        );
175
176        // Ensure we have one and only one event ready for consumption.
177        let Poll::Ready((event, metric_event_request_stream)) =
178            exec.run_until_stalled(&mut metric_event_request_future)
179        else {
180            panic!("Failed to receive metrics")
181        };
182        let event = event.ok_or_else(|| anyhow!("Metrics stream unexpectedly closed"))??;
183        match event {
184            fmetrics::MetricEventLoggerRequest::LogMetricEvents { events, responder, .. } => {
185                assert_eq!(events.len(), 3);
186                // Kernel metrics
187                assert_eq!(
188                    events[0],
189                    fmetrics::MetricEvent {
190                        metric_id: cobalt_registry::MEMORY_STALLS_PER_HOUR_METRIC_ID,
191                        event_codes: vec![
192                            cobalt_registry::MemoryMetricDimensionStallType::Some.as_event_code()
193                        ],
194                        payload: fmetrics::MetricEventPayload::IntegerValue(10)
195                    }
196                );
197                assert_eq!(
198                    events[1],
199                    fmetrics::MetricEvent {
200                        metric_id: cobalt_registry::MEMORY_STALLS_PER_HOUR_METRIC_ID,
201                        event_codes: vec![
202                            cobalt_registry::MemoryMetricDimensionStallType::Full.as_event_code()
203                        ],
204                        payload: fmetrics::MetricEventPayload::IntegerValue(20)
205                    }
206                );
207                assert_eq!(
208                    events[2],
209                    fmetrics::MetricEvent {
210                        metric_id: cobalt_registry::MEMORY_PAGE_REFAULTS_PER_HOUR_METRIC_ID,
211                        event_codes: vec![],
212                        payload: fmetrics::MetricEventPayload::IntegerValue(100)
213                    }
214                );
215                responder.send(Ok(()))?;
216            }
217            _ => panic!("Unexpected metric event"),
218        }
219
220        let mut metric_event_request_future = metric_event_request_stream.into_future();
221
222        assert!(exec.run_until_stalled(&mut metric_event_request_future).is_pending());
223
224        // Advance to the next hour
225        assert!(
226            exec.run_until_stalled(&mut std::pin::pin!(fasync::TestExecutor::advance_to(
227                (zx::MonotonicInstant::ZERO + zx::Duration::from_seconds(60 * 60 * 2 + 10)).into()
228            )))
229            .is_ready(),
230            "Failed to advance time"
231        );
232
233        // Ensure we have one and only one event ready for consumption.
234        let Poll::Ready((event, metric_event_request_stream)) =
235            exec.run_until_stalled(&mut metric_event_request_future)
236        else {
237            panic!("Failed to receive metrics")
238        };
239        let event = event.ok_or_else(|| anyhow!("Metrics stream unexpectedly closed"))??;
240        match event {
241            fmetrics::MetricEventLoggerRequest::LogMetricEvents { events, responder, .. } => {
242                assert_eq!(events.len(), 3);
243                // Kernel metrics
244                assert_eq!(
245                    events[0],
246                    fmetrics::MetricEvent {
247                        metric_id: cobalt_registry::MEMORY_STALLS_PER_HOUR_METRIC_ID,
248                        event_codes: vec![
249                            cobalt_registry::MemoryMetricDimensionStallType::Some.as_event_code()
250                        ],
251                        payload: fmetrics::MetricEventPayload::IntegerValue(10)
252                    }
253                );
254                assert_eq!(
255                    events[1],
256                    fmetrics::MetricEvent {
257                        metric_id: cobalt_registry::MEMORY_STALLS_PER_HOUR_METRIC_ID,
258                        event_codes: vec![
259                            cobalt_registry::MemoryMetricDimensionStallType::Full.as_event_code()
260                        ],
261                        payload: fmetrics::MetricEventPayload::IntegerValue(20)
262                    }
263                );
264                assert_eq!(
265                    events[2],
266                    fmetrics::MetricEvent {
267                        metric_id: cobalt_registry::MEMORY_PAGE_REFAULTS_PER_HOUR_METRIC_ID,
268                        event_codes: vec![],
269                        payload: fmetrics::MetricEventPayload::IntegerValue(250)
270                    }
271                );
272                responder.send(Ok(()))?;
273            }
274            _ => panic!("Unexpected metric event"),
275        }
276
277        assert!(
278            exec.run_until_stalled(&mut metric_event_request_stream.into_future()).is_pending()
279        );
280
281        Ok(())
282    }
283}