Skip to main content

periodic_monitoring/
lib.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.
4use anyhow::{Context, Result};
5use attribution_processing::digest::{BucketDefinition, Digest};
6use attribution_processing::summary::MemorySummary;
7use attribution_processing::{
8    AttributionData, AttributionDataProvider, ProcessedAttributionData, attribute_vmos,
9};
10use fuchsia_async::WakeupTime;
11use fuchsia_inspect::{ArrayProperty, Node, StringProperty};
12use fuchsia_inspect_contrib::nodes::BoundedListNode;
13use fuchsia_trace::{category_enabled, duration};
14use fuchsia_trace_observer::TraceObserver;
15use futures::stream::FusedStream;
16use futures::{FutureExt, Stream, StreamExt, TryFutureExt, join, select, try_join};
17use humansize::{BINARY, FormatSizeOptions, format_size};
18use stalls::StallProvider;
19use stalls::refaults::PageFaultProvider;
20use std::future::Future;
21use traces::{CATEGORY_MEMORY_CAPTURE, CATEGORY_MEMORY_SUPPRESS_PERIODIC};
22
23use fidl_fuchsia_kernel as fkernel;
24use fidl_fuchsia_metrics as fmetrics;
25
26const PERIODIC_MONITORING_INTERVAL: zx::MonotonicDuration = zx::MonotonicDuration::from_minutes(5);
27
28/// Waits while periodic monitoring is suppressed (`is_suppressed() == true`), awaiting
29/// `on_state_changed()` between checks. Breaks out and falls through if `on_state_changed()`
30/// returns an error.
31async fn wait_while_periodic_suppressed<Fut, T, E>(
32    mut is_suppressed: impl FnMut() -> bool,
33    mut on_state_changed: impl FnMut() -> Fut,
34) where
35    Fut: Future<Output = Result<T, E>>,
36{
37    while is_suppressed() {
38        if on_state_changed().await.is_err() {
39            break;
40        }
41    }
42}
43
44/// Data collected during an on-demand snapshot that can be reused to update Inspect historical
45/// data without performing a redundant periodic collection.
46pub struct ManualMonitoringData {
47    pub timestamp: zx::BootInstant,
48    pub attribution_data: AttributionData,
49    pub kmem_stats: fkernel::MemoryStats,
50    pub kmem_stats_compression: fkernel::MemoryStatsCompression,
51    pub done_tx: Option<futures::channel::oneshot::Sender<()>>,
52}
53
54/// Notification sent by the on-demand snapshot handler to `periodic_monitoring`.
55pub enum ManualMonitoringEvent {
56    /// Signals that an on-demand snapshot has started, resetting the periodic timer immediately
57    /// so a periodic collection does not race with the in-progress snapshot.
58    Started,
59    /// Delivers the collected snapshot data to be recorded in Inspect history and resets the
60    /// periodic timer.
61    Completed(Box<ManualMonitoringData>),
62}
63
64/// Waits for [PERIODIC_MONITORING_INTERVAL], restarting the countdown when an on-demand snapshot
65/// starts and returning `Some(data)` immediately if a completed manual snapshot newer than
66/// `last_timestamp` arrives to be recorded in Inspect history, or `None` when the periodic timer
67/// fires. Queued events are drained and coalesced so only the newest `Completed` snapshot is
68/// processed.
69async fn wait_for_next_collection(
70    manual_events: &mut (impl FusedStream<Item = ManualMonitoringEvent> + Unpin),
71    last_timestamp: zx::BootInstant,
72) -> Option<Box<ManualMonitoringData>> {
73    let mut timer = std::pin::pin!(PERIODIC_MONITORING_INTERVAL.into_timer().fuse());
74    loop {
75        select! {
76            _ = timer => return None,
77            first_event = manual_events.select_next_some() => {
78                timer.set(PERIODIC_MONITORING_INTERVAL.into_timer().fuse());
79                let mut latest_completed: Option<Box<ManualMonitoringData>> = None;
80                let mut next_event = Some(first_event);
81                while let Some(event) = next_event {
82                    if let ManualMonitoringEvent::Completed(data) = event {
83                        let is_newer_than_last = data.timestamp > last_timestamp;
84                        let is_newer_than_candidate = latest_completed
85                            .as_ref()
86                            .is_none_or(|prev| data.timestamp >= prev.timestamp);
87                        if is_newer_than_last && is_newer_than_candidate {
88                            latest_completed = Some(data);
89                        }
90                    }
91                    next_event = if manual_events.is_terminated() {
92                        None
93                    } else {
94                        manual_events.next().now_or_never().flatten()
95                    };
96                }
97                if latest_completed.is_some() {
98                    return latest_completed;
99                }
100            }
101        }
102    }
103}
104
105/// Periodically collect and report memory attribution data.
106///
107/// This produces a simplified schedule where, instead of having
108/// independent reports on their own cadence, we collect based on
109/// cobalt's frequency and produce all other reports based on that one
110/// collection, saving significant CPU at the expense of flexibility.
111pub async fn periodic_monitoring(
112    kernel_stats_proxy: fkernel::StatsProxy,
113    attribution_data_service: &impl AttributionDataProvider,
114    stall_provider: &impl StallProvider,
115    page_fault_provider: &impl PageFaultProvider,
116    metric_event_logger: &fmetrics::MetricEventLoggerProxy,
117    bucket_definitions: &[BucketDefinition],
118    inspect_root: Node,
119    manual_events: impl Stream<Item = ManualMonitoringEvent> + Unpin,
120) -> Result<()> {
121    let mut _current: Option<StringProperty> = None; // Ensure the inspect property is kept as long as necessary.
122    let mut bucket_list_node = std::cell::OnceCell::new();
123    let bucket_names = std::cell::OnceCell::new();
124    let bucket_codes = cobalt::prepare_bucket_codes(bucket_definitions);
125    let trace_observer = TraceObserver::new();
126    let mut manual_events = manual_events.fuse();
127    let mut manual_collection: Option<Box<ManualMonitoringData>> = None;
128    loop {
129        if manual_collection.is_none() {
130            // Defer periodic collection while a trace session with `memory:suppress_periodic` is
131            // actively recording so the expensive VMO/VMAR walk does not perturb traced benchmarks.
132            wait_while_periodic_suppressed(
133                || category_enabled(CATEGORY_MEMORY_SUPPRESS_PERIODIC),
134                || trace_observer.on_state_changed(),
135            )
136            .await;
137        }
138        let mut manual_done_tx = None;
139        let timestamp = {
140            duration!(CATEGORY_MEMORY_CAPTURE, c"periodic_monitoring");
141            let (timestamp, kmem_stats, kmem_stats_compression, raw_attribution_data) =
142                match manual_collection.take() {
143                    None => {
144                        let timestamp = zx::BootInstant::get();
145                        // Retrieve (concurrently) the data necessary to perform the aggregation.
146                        let (kmem_stats, kmem_stats_compression) = try_join!(
147                            kernel_stats_proxy.get_memory_stats().map_err(anyhow::Error::from),
148                            kernel_stats_proxy
149                                .get_memory_stats_compression()
150                                .map_err(anyhow::Error::from)
151                        )
152                        .with_context(|| "Failed to get kernel memory stats")?;
153                        // This is the very expensive operation.
154                        let raw_attribution_data =
155                            attribution_data_service.get_attribution_data()?;
156                        (timestamp, kmem_stats, kmem_stats_compression, raw_attribution_data)
157                    }
158                    Some(manual) => {
159                        let ManualMonitoringData {
160                            timestamp,
161                            attribution_data,
162                            kmem_stats,
163                            kmem_stats_compression,
164                            done_tx,
165                        } = *manual;
166                        manual_done_tx = done_tx;
167                        (timestamp, kmem_stats, kmem_stats_compression, attribution_data)
168                    }
169                };
170            let attribution_data = attribute_vmos(raw_attribution_data);
171            let digest = Digest::compute(
172                &attribution_data,
173                &kmem_stats,
174                &kmem_stats_compression,
175                bucket_definitions,
176                false,
177            )?;
178            drop(_current.take());
179            _current = Some(update_inspect_summary(
180                attribution_data,
181                timestamp,
182                &kmem_stats,
183                &inspect_root,
184            ));
185            cobalt::upload_metrics(
186                timestamp,
187                &kmem_stats,
188                metric_event_logger,
189                &digest,
190                &bucket_codes,
191            )
192            .await?;
193            {
194                // Initialize the inspect property containing the buckets names, if necessary.
195                let _ = bucket_names.get_or_init(|| {
196                    // Create inspect node to store buckets related information.
197                    let bucket_names =
198                        inspect_root.create_string_array("buckets", digest.buckets.len());
199                    for (i, attribution_processing::digest::Bucket { name, .. }) in
200                        digest.buckets.iter().enumerate()
201                    {
202                        bucket_names.set(i, name);
203                    }
204                    bucket_names
205                });
206            }
207            update_inspect_history(
208                timestamp,
209                &digest,
210                stall_provider,
211                page_fault_provider,
212                &mut bucket_list_node,
213                &inspect_root,
214            )?;
215            timestamp
216        };
217        let (_, next) = join!(
218            fuchsia_async::Task::local(async move {
219                let _ = scudo::mallopt(scudo::M_PURGE_ALL, 0);
220                if let Some(done_tx) = manual_done_tx {
221                    let _ = done_tx.send(());
222                }
223            }),
224            wait_for_next_collection(&mut manual_events, timestamp)
225        );
226        manual_collection = next;
227    }
228}
229
230fn update_inspect_summary(
231    attribution_data: ProcessedAttributionData,
232    timestamp: zx::BootInstant,
233    kmem_stats: &fkernel::MemoryStats,
234    inspect_root: &Node,
235) -> StringProperty {
236    inspect_root.create_string(
237        "current",
238        record_summary(attribution_data.summary(), timestamp, &kmem_stats),
239    )
240}
241
242/// Update inspect data with collected memory information.
243fn update_inspect_history(
244    timestamp: zx::BootInstant,
245    digest: &Digest,
246    stall_provider: &impl StallProvider,
247    page_fault_provider: &impl PageFaultProvider,
248    bucket_list_node: &mut std::cell::OnceCell<BoundedListNode>,
249    inspect_root: &Node,
250) -> Result<()> {
251    let stall_values =
252        stall_provider.get_stall_info().with_context(|| "Unable to retrieve stall information")?;
253    let page_faults = page_fault_provider.get_fault_count();
254    let page_refaults = page_fault_provider.get_refault_count();
255    // Add an entry for the current aggregation.
256    let _ = bucket_list_node
257        .get_or_init(|| BoundedListNode::new(inspect_root.create_child("measurements"), 100));
258    bucket_list_node.get_mut().unwrap().add_entry(|n| {
259        n.record_int("timestamp", timestamp.into_nanos());
260        {
261            let committed_sizes = n.create_uint_array("bucket_sizes", digest.buckets.len());
262            let populated_sizes =
263                n.create_uint_array("bucket_sizes_populated", digest.buckets.len());
264            for (i, b) in digest.buckets.iter().enumerate() {
265                committed_sizes.set(i, b.committed_size as u64);
266                populated_sizes.set(i, b.populated_size as u64);
267            }
268            n.record(committed_sizes);
269            n.record(populated_sizes);
270        }
271
272        n.record_child("stalls", |child| {
273            child.record_uint(
274                "some_ms",
275                stall_values.some.as_millis().try_into().unwrap_or(u64::MAX),
276            );
277            child.record_uint(
278                "full_ms",
279                stall_values.full.as_millis().try_into().unwrap_or(u64::MAX),
280            );
281        });
282
283        n.record_child("faults", |child| {
284            child.record_uint("count", page_faults);
285        });
286
287        n.record_child("refaults", |child| {
288            child.record_uint("count", page_refaults);
289        });
290    });
291    Ok(())
292}
293
294fn record_summary(
295    mut summary: MemorySummary,
296    timestamp: zx::Instant<zx::BootTimeline>,
297    kmem_stats: &fkernel::MemoryStats,
298) -> String {
299    let size_options = FormatSizeOptions::from(BINARY).space_after_value(false);
300    summary.principals.sort_by_key(|p| std::cmp::Reverse(p.populated_private));
301    format!(
302        "Time: {} VMO: {} Free: {}\n{}",
303        timestamp.into_nanos(),
304        kmem_stats
305            .vmo_bytes
306            .and_then(|b| Some(format_size(b, size_options)))
307            .unwrap_or_else(|| "?".to_string()),
308        kmem_stats
309            .free_bytes
310            .and_then(|b| Some(format_size(b, size_options)))
311            .unwrap_or_else(|| "?".to_string()),
312        summary
313            .principals
314            .iter_mut()
315            .filter_map(|principal| {
316                if principal.populated_total == 0 {
317                    return None;
318                }
319                let (populated_private, populated_scaled, populated_total) = match (|| {
320                    Some((
321                        format_size(principal.populated_private, size_options),
322                        format_size(principal.populated_scaled as u64, size_options),
323                        format_size(principal.populated_total, size_options),
324                    ))
325                })(
326                ) {
327                    Some(ok) => ok,
328                    None => return None,
329                };
330                let mut vmos = principal.vmos.iter().collect::<Vec<_>>();
331                vmos.sort_by_key(|(_, vmo)| {
332                    std::cmp::Reverse((vmo.committed_private, vmo.committed_scaled as u64))
333                });
334                let sizes = if populated_total == populated_private {
335                    format_args!("{}", populated_total)
336                } else {
337                    format_args!("{} {} {}", populated_private, populated_scaled, populated_total)
338                };
339                Some(format!(
340                    "{}: {}; {}",
341                    principal.name,
342                    sizes,
343                    vmos.iter()
344                        .filter_map(|(name, vmo)| {
345                            if vmo.committed_total == 0 {
346                                None
347                            } else {
348                                Some(format!(
349                                    "{} {} {} {}",
350                                    name,
351                                    format_size(vmo.populated_private, size_options),
352                                    format_size(vmo.populated_scaled as u64, size_options),
353                                    format_size(vmo.populated_total, size_options)
354                                ))
355                            }
356                        })
357                        .collect::<Vec<_>>()
358                        .join("; ")
359                ))
360            })
361            .collect::<Vec<_>>()
362            .join("\n")
363    )
364}
365#[cfg(test)]
366mod tests {
367    use super::*;
368    use attribution_processing::{
369        Attribution, AttributionData, GlobalPrincipalIdentifier, Principal, PrincipalDescription,
370        PrincipalType, Resource, ResourceReference, ZXName,
371    };
372    use diagnostics_assertions::{NonZeroIntProperty, assert_data_tree};
373    use std::num::NonZero;
374    use std::time::Duration;
375
376    use fidl_fuchsia_memory_attribution_plugin as fplugin;
377
378    fn get_kernel_stats() -> (fkernel::MemoryStats, fkernel::MemoryStatsCompression) {
379        (
380            fkernel::MemoryStats {
381                total_bytes: Some(1),
382                free_bytes: Some(2),
383                wired_bytes: Some(3),
384                total_heap_bytes: Some(4),
385                free_heap_bytes: Some(5),
386                vmo_bytes: Some(6),
387                mmu_overhead_bytes: Some(7),
388                ipc_bytes: Some(8),
389                other_bytes: Some(9),
390                free_loaned_bytes: Some(10),
391                cache_bytes: Some(11),
392                slab_bytes: Some(12),
393                zram_bytes: Some(13),
394                vmo_reclaim_total_bytes: Some(14),
395                vmo_reclaim_newest_bytes: Some(15),
396                vmo_reclaim_oldest_bytes: Some(16),
397                vmo_reclaim_disabled_bytes: Some(17),
398                vmo_discardable_locked_bytes: Some(18),
399                vmo_discardable_unlocked_bytes: Some(19),
400                ..Default::default()
401            },
402            fkernel::MemoryStatsCompression {
403                uncompressed_storage_bytes: Some(20),
404                compressed_storage_bytes: Some(21),
405                compressed_fragmentation_bytes: Some(22),
406                compression_time: Some(23),
407                decompression_time: Some(24),
408                total_page_compression_attempts: Some(25),
409                failed_page_compression_attempts: Some(26),
410                total_page_decompressions: Some(27),
411                compressed_page_evictions: Some(28),
412                eager_page_compressions: Some(29),
413                memory_pressure_page_compressions: Some(30),
414                critical_memory_page_compressions: Some(31),
415                pages_decompressed_unit_ns: Some(32),
416                pages_decompressed_within_log_time: Some([40, 41, 42, 43, 44, 45, 46, 47]),
417
418                ..Default::default()
419            },
420        )
421    }
422
423    fn get_raw_attribution_data() -> AttributionData {
424        AttributionData {
425            principals_vec: vec![Principal {
426                identifier: GlobalPrincipalIdentifier(NonZero::new(1).unwrap()),
427                description: Some(PrincipalDescription::Component("principal".to_owned())),
428                principal_type: PrincipalType::Runnable,
429                parent: None,
430            }],
431            resources_vec: vec![Resource {
432                koid: 10,
433                name_index: 0,
434                resource_type: fplugin::ResourceType::Vmo(fplugin::Vmo {
435                    parent: None,
436                    private_committed_bytes: Some(1024),
437                    private_populated_bytes: Some(2048),
438                    scaled_committed_bytes: Some(1024),
439                    scaled_populated_bytes: Some(2048),
440                    total_committed_bytes: Some(1024),
441                    total_populated_bytes: Some(2048),
442                    ..Default::default()
443                }),
444            }],
445            resource_names: vec![ZXName::from_string_lossy("resource")],
446            attributions: vec![Attribution {
447                source: GlobalPrincipalIdentifier(NonZero::new(1).unwrap()),
448                subject: GlobalPrincipalIdentifier(NonZero::new(1).unwrap()),
449                resources: vec![ResourceReference::KernelObject(10)],
450            }],
451        }
452    }
453
454    fn get_attribution_data() -> ProcessedAttributionData {
455        attribute_vmos(get_raw_attribution_data())
456    }
457
458    #[derive(Clone)]
459    struct FakeStallProvider {}
460    impl StallProvider for FakeStallProvider {
461        fn get_stall_info(&self) -> Result<stalls::MemoryStallMetrics, anyhow::Error> {
462            Ok(stalls::MemoryStallMetrics {
463                some: Duration::from_millis(10),
464                full: Duration::from_millis(20),
465            })
466        }
467    }
468
469    #[derive(Clone)]
470    struct FakePageFaultProvider {
471        refault_count: u64,
472        fault_count: u64,
473    }
474
475    impl PageFaultProvider for FakePageFaultProvider {
476        fn get_refault_count(&self) -> u64 {
477            self.refault_count
478        }
479
480        fn get_fault_count(&self) -> u64 {
481            self.fault_count
482        }
483    }
484
485    #[fuchsia::test]
486    async fn test_update_inspect() -> Result<()> {
487        let inspector = fuchsia_inspect::Inspector::default();
488        let digest_node = inspector.root().create_child("logger");
489        let timestamp = zx::BootInstant::get();
490        let attribution_data = get_attribution_data();
491        let (kernel_stats, kernel_stats_compression) = get_kernel_stats();
492        let digest = Digest::compute(
493            &attribution_data,
494            &kernel_stats,
495            &kernel_stats_compression,
496            &vec![],
497            false,
498        )?;
499        let mut bucket_list_node = std::cell::OnceCell::new();
500        // Update inspect history twice, and ensure both instances are recorded.
501        let _summary =
502            update_inspect_summary(attribution_data, timestamp, &kernel_stats, &digest_node);
503        update_inspect_history(
504            timestamp,
505            &digest,
506            &FakeStallProvider {},
507            &FakePageFaultProvider { refault_count: 300, fault_count: 400 },
508            &mut bucket_list_node,
509            &digest_node,
510        )?;
511
512        update_inspect_history(
513            timestamp,
514            &digest,
515            &FakeStallProvider {},
516            &FakePageFaultProvider { refault_count: 700, fault_count: 800 },
517            &mut bucket_list_node,
518            &digest_node,
519        )?;
520        assert_data_tree!(inspector, root: {
521            logger: {
522                measurements: {
523                    // First update.
524                    "0": {
525                        timestamp: NonZeroIntProperty,
526                        bucket_sizes: vec![
527                            1024u64, // Undigested: matches the single unmatched VMO
528                            // Orphaned: vmo_bytes reported by the kernel but not covered by any
529                            // bucket => 6 - 1024 => 0 (saturating, cannot be negative)
530                            0u64,
531                            54u64,   // Kernel: 3 wired + 4 heap + 7 mmu + 8 IPC + 9 other + 12 slab + 11 cache = 54
532                            2u64,    // Free
533                            14u64,   // [Addl]PagerTotal
534                            15u64,   // [Addl]PagerNewest
535                            16u64,   // [Addl]PagerOldest
536                            18u64,   // [Addl]DiscardableLocked
537                            19u64,   // [Addl]DiscardableUnlocked
538                            21u64,   // [Addl]ZramCompressedBytes
539                            6u64,    // [Addl]PopulatedAnonymousBytes
540                        ],
541                        bucket_sizes_populated: vec![
542                            2048u64, // Undigested: matches the single unmatched VMO
543                            // Orphaned: vmo_bytes reported by the kernel but not covered by any
544                            // bucket => 6 - 1024 => 0 (saturating, cannot be negative)
545                            0u64,
546                            54u64,   // Kernel: 3 wired + 4 heap + 7 mmu + 8 IPC + 9 other + 12 slab + 11 cache = 54
547                            2u64,    // Free
548                            14u64,   // [Addl]PagerTotal
549                            15u64,   // [Addl]PagerNewest
550                            16u64,   // [Addl]PagerOldest
551                            18u64,   // [Addl]DiscardableLocked
552                            19u64,   // [Addl]DiscardableUnlocked
553                            21u64,   // [Addl]ZramCompressedBytes
554                            6u64,    // [Addl]PopulatedAnonymousBytes
555                        ],
556
557                        stalls: {
558                            some_ms: 10u64,
559                            full_ms: 20u64,
560                        },
561                        faults: {
562                            count: 400u64,
563                        },
564                        refaults: {
565                            count: 300u64,
566                        },
567                    },
568                    // Second update.
569                    "1": {
570                        timestamp: NonZeroIntProperty,
571                        bucket_sizes: vec![
572                            1024u64, // Undigested: matches the single unmatched VMO
573                            // Orphaned: vmo_bytes reported by the kernel but not covered by any
574                            // bucket => 6 - 1024 => 0 (saturating, cannot be negative)
575                            0u64,
576                            54u64,   // Kernel: 3 wired + 4 heap + 7 mmu + 8 IPC + 9 other + 12 slab + 11 cache = 54
577                            2u64,    // Free
578                            14u64,   // [Addl]PagerTotal
579                            15u64,   // [Addl]PagerNewest
580                            16u64,   // [Addl]PagerOldest
581                            18u64,   // [Addl]DiscardableLocked
582                            19u64,   // [Addl]DiscardableUnlocked
583                            21u64,   // [Addl]ZramCompressedBytes
584                            6u64,    // [Addl]PopulatedAnonymousBytes
585                        ],
586                        bucket_sizes_populated: vec![
587                            2048u64, // Undigested: matches the single unmatched VMO
588                            // Orphaned: vmo_bytes reported by the kernel but not covered by any
589                            // bucket => 6 - 1024 => 0 (saturating, cannot be negative)
590                            0u64,
591                            54u64,   // Kernel: 3 wired + 4 heap + 7 mmu + 8 IPC + 9 other + 12 slab + 11 cache = 54
592                            2u64,    // Free
593                            14u64,   // [Addl]PagerTotal
594                            15u64,   // [Addl]PagerNewest
595                            16u64,   // [Addl]PagerOldest
596                            18u64,   // [Addl]DiscardableLocked
597                            19u64,   // [Addl]DiscardableUnlocked
598                            21u64,   // [Addl]ZramCompressedBytes
599                            6u64,    // [Addl]PopulatedAnonymousBytes
600                        ],
601                        stalls: {
602                            some_ms: 10u64,
603                            full_ms: 20u64,
604                        },
605                        faults: {
606                            count: 800u64,
607                        },
608                        refaults: {
609                            count: 700u64,
610                        },
611                    },
612                },
613                current: regex::Regex::new(r"^Time: \d+ VMO: 6B Free: 2B\nprincipal: 2KiB; resource 2KiB 2KiB 2KiB")?,
614            },
615        });
616        Ok(())
617    }
618
619    #[test]
620    fn test_wait_while_periodic_suppressed_defers_while_category_enabled() {
621        use std::cell::{Cell, RefCell};
622
623        let mut exec = fuchsia_async::TestExecutor::new();
624        let is_suppressed = Cell::new(true);
625        let (tx, rx) = futures::channel::mpsc::unbounded::<Result<(), zx::Status>>();
626        let rx = RefCell::new(rx);
627
628        {
629            let mut wait_fut = std::pin::pin!(wait_while_periodic_suppressed(
630                || is_suppressed.get(),
631                || async { rx.borrow_mut().next().await.unwrap_or(Err(zx::Status::PEER_CLOSED)) },
632            ));
633
634            // While `memory:suppress_periodic` is enabled, the future stays pending.
635            assert!(exec.run_until_stalled(&mut wait_fut).is_pending());
636
637            // A state change notification while `memory:suppress_periodic` is still enabled
638            // keeps waiting.
639            tx.unbounded_send(Ok(())).unwrap();
640            assert!(exec.run_until_stalled(&mut wait_fut).is_pending());
641
642            // Once the trace stops (or `memory:suppress_periodic` is disabled) and the observer
643            // signals, the wait completes.
644            is_suppressed.set(false);
645            tx.unbounded_send(Ok(())).unwrap();
646            assert!(exec.run_until_stalled(&mut wait_fut).is_ready());
647        }
648
649        // If `on_state_changed` returns an error while suppressed, `wait_while_periodic_suppressed`
650        // breaks out of the loop and completes instead of busy-waiting.
651        is_suppressed.set(true);
652        let mut wait_fut = std::pin::pin!(wait_while_periodic_suppressed(
653            || is_suppressed.get(),
654            || async { rx.borrow_mut().next().await.unwrap_or(Err(zx::Status::PEER_CLOSED)) },
655        ));
656        assert!(exec.run_until_stalled(&mut wait_fut).is_pending());
657        tx.unbounded_send(Err(zx::Status::BAD_STATE)).unwrap();
658        assert!(exec.run_until_stalled(&mut wait_fut).is_ready());
659    }
660
661    struct CountingAttributionDataProvider {
662        call_count: std::sync::atomic::AtomicUsize,
663    }
664
665    impl AttributionDataProvider for CountingAttributionDataProvider {
666        fn get_attribution_data(&self) -> Result<AttributionData, anyhow::Error> {
667            self.call_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
668            Ok(get_raw_attribution_data())
669        }
670    }
671
672    #[test]
673    fn test_periodic_monitoring_reuses_manual_and_resets_timer() -> Result<()> {
674        let mut exec = fuchsia_async::TestExecutor::new_with_fake_time();
675        let start = exec.now();
676
677        let (kernel_stats_proxy, mut kernel_stats_stream) =
678            fidl::endpoints::create_proxy_and_stream::<fkernel::StatsMarker>();
679        let _kernel_stats_task = fuchsia_async::Task::local(async move {
680            while let Some(Ok(request)) = kernel_stats_stream.next().await {
681                let (mem_stats, comp_stats) = get_kernel_stats();
682                match request {
683                    fkernel::StatsRequest::GetMemoryStats { responder } => {
684                        let _ = responder.send(&mem_stats);
685                    }
686                    fkernel::StatsRequest::GetMemoryStatsCompression { responder } => {
687                        let _ = responder.send(&comp_stats);
688                    }
689                    _ => unreachable!(),
690                }
691            }
692        });
693
694        let (metric_event_logger, mut metric_event_stream) =
695            fidl::endpoints::create_proxy_and_stream::<fmetrics::MetricEventLoggerMarker>();
696        let _metrics_task = fuchsia_async::Task::local(async move {
697            while let Some(Ok(request)) = metric_event_stream.next().await {
698                if let fmetrics::MetricEventLoggerRequest::LogMetricEvents { responder, .. } =
699                    request
700                {
701                    let _ = responder.send(Ok(()));
702                }
703            }
704        });
705
706        let inspector = fuchsia_inspect::Inspector::default();
707        let attribution_provider =
708            CountingAttributionDataProvider { call_count: std::sync::atomic::AtomicUsize::new(0) };
709        let stall_provider = FakeStallProvider {};
710        let page_fault_provider = FakePageFaultProvider { refault_count: 300, fault_count: 400 };
711        let (tx, rx) = futures::channel::mpsc::unbounded::<ManualMonitoringEvent>();
712
713        let mut monitoring_fut = std::pin::pin!(periodic_monitoring(
714            kernel_stats_proxy,
715            &attribution_provider,
716            &stall_provider,
717            &page_fault_provider,
718            &metric_event_logger,
719            &[],
720            inspector.root().create_child("logger"),
721            rx,
722        ));
723
724        // Initial periodic collection runs immediately at t = 0.
725        assert!(exec.run_until_stalled(&mut monitoring_fut).is_pending());
726        assert_eq!(attribution_provider.call_count.load(std::sync::atomic::Ordering::SeqCst), 1);
727
728        // Advance 4 minutes (< 5 minutes): no new periodic collection yet.
729        exec.set_fake_time(start + fuchsia_async::MonotonicDuration::from_minutes(4));
730        assert!(exec.run_until_stalled(&mut monitoring_fut).is_pending());
731        assert_eq!(attribution_provider.call_count.load(std::sync::atomic::Ordering::SeqCst), 1);
732
733        // Signal an on-demand snapshot start at t = 4m, resetting the 5-minute countdown.
734        tx.unbounded_send(ManualMonitoringEvent::Started).unwrap();
735        assert!(exec.run_until_stalled(&mut monitoring_fut).is_pending());
736
737        // At t = 6m (past the initial 5m deadline, but only 2m after the reset), no collection.
738        exec.set_fake_time(start + fuchsia_async::MonotonicDuration::from_minutes(6));
739        assert!(exec.run_until_stalled(&mut monitoring_fut).is_pending());
740        assert_eq!(attribution_provider.call_count.load(std::sync::atomic::Ordering::SeqCst), 1);
741
742        // At t = 9m (5m after the reset at t = 4m), the periodic timer fires.
743        exec.set_fake_time(start + fuchsia_async::MonotonicDuration::from_minutes(9));
744        assert!(exec.run_until_stalled(&mut monitoring_fut).is_pending());
745        assert_eq!(attribution_provider.call_count.load(std::sync::atomic::Ordering::SeqCst), 2);
746
747        // At t = 10m, send a stale manual collection (timestamp = 1ns) followed by two new
748        // manual collections back-to-back; only the newest valid one should be recorded, and
749        // `get_attribution_data()` must NOT be called again.
750        exec.set_fake_time(start + fuchsia_async::MonotonicDuration::from_minutes(10));
751        let (kmem_stats, kmem_stats_compression) = get_kernel_stats();
752        let (stale_done_tx, mut stale_done_rx) = futures::channel::oneshot::channel();
753        tx.unbounded_send(ManualMonitoringEvent::Completed(Box::new(ManualMonitoringData {
754            timestamp: zx::BootInstant::from_nanos(1),
755            attribution_data: get_raw_attribution_data(),
756            kmem_stats: kmem_stats.clone(),
757            kmem_stats_compression: kmem_stats_compression.clone(),
758            done_tx: Some(stale_done_tx),
759        })))
760        .unwrap();
761
762        let base_ts = zx::BootInstant::get() + zx::BootDuration::from_seconds(10);
763        let newer_ts = base_ts + zx::BootDuration::from_seconds(1);
764        let (superseded_done_tx, mut superseded_done_rx) = futures::channel::oneshot::channel();
765        tx.unbounded_send(ManualMonitoringEvent::Completed(Box::new(ManualMonitoringData {
766            timestamp: base_ts,
767            attribution_data: get_raw_attribution_data(),
768            kmem_stats: kmem_stats.clone(),
769            kmem_stats_compression: kmem_stats_compression.clone(),
770            done_tx: Some(superseded_done_tx),
771        })))
772        .unwrap();
773
774        let (done_tx, mut done_rx) = futures::channel::oneshot::channel();
775        tx.unbounded_send(ManualMonitoringEvent::Completed(Box::new(ManualMonitoringData {
776            timestamp: newer_ts,
777            attribution_data: get_raw_attribution_data(),
778            kmem_stats,
779            kmem_stats_compression,
780            done_tx: Some(done_tx),
781        })))
782        .unwrap();
783
784        assert!(exec.run_until_stalled(&mut monitoring_fut).is_pending());
785        // Manual reuse must not invoke `get_attribution_data()`.
786        assert_eq!(attribution_provider.call_count.load(std::sync::atomic::Ordering::SeqCst), 2);
787        // Completion acks for the processed and dropped snapshots have all resolved.
788        assert!(matches!(done_rx.try_recv(), Ok(Some(()))));
789        assert!(stale_done_rx.try_recv().is_err());
790        assert!(superseded_done_rx.try_recv().is_err());
791
792        // Next periodic collection is scheduled 5m after the manual completion at t = 10m:
793        // at t = 14m it has not fired yet, and at t = 15m it fires.
794        exec.set_fake_time(start + fuchsia_async::MonotonicDuration::from_minutes(14));
795        assert!(exec.run_until_stalled(&mut monitoring_fut).is_pending());
796        assert_eq!(attribution_provider.call_count.load(std::sync::atomic::Ordering::SeqCst), 2);
797
798        exec.set_fake_time(start + fuchsia_async::MonotonicDuration::from_minutes(15));
799        assert!(exec.run_until_stalled(&mut monitoring_fut).is_pending());
800        assert_eq!(attribution_provider.call_count.load(std::sync::atomic::Ordering::SeqCst), 3);
801
802        drop(monitoring_fut);
803
804        // Verify Inspect tree has `buckets`, `current`, and 4 measurements ("0" from t=0,
805        // "1" from t=9m, "2" from the coalesced manual snapshot at `newer_ts`, and "3" from t=15m).
806        let mut assert_fut = std::pin::pin!(async {
807            assert_data_tree!(inspector, root: {
808                logger: contains {
809                    buckets: vec![
810                        "Undigested",
811                        "Orphaned",
812                        "Kernel",
813                        "Free",
814                        "[Addl]PagerTotal",
815                        "[Addl]PagerNewest",
816                        "[Addl]PagerOldest",
817                        "[Addl]DiscardableLocked",
818                        "[Addl]DiscardableUnlocked",
819                        "[Addl]ZramCompressedBytes",
820                        "[Addl]PopulatedAnonymousBytes",
821                    ],
822                    current: regex::Regex::new(r"^Time: \d+ VMO: 6B Free: 2B\nprincipal: 2KiB; resource 2KiB 2KiB 2KiB").unwrap(),
823                    measurements: {
824                        "0": contains {
825                            timestamp: NonZeroIntProperty,
826                        },
827                        "1": contains {
828                            timestamp: NonZeroIntProperty,
829                        },
830                        "2": contains {
831                            timestamp: newer_ts.into_nanos(),
832                        },
833                        "3": contains {
834                            timestamp: NonZeroIntProperty,
835                        },
836                    },
837                },
838            });
839        });
840        assert!(exec.run_until_stalled(&mut assert_fut).is_ready());
841        Ok(())
842    }
843}