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::{AttributionDataProvider, ProcessedAttributionData, attribute_vmos};
8use fuchsia_async::WakeupTime;
9use fuchsia_inspect::{ArrayProperty, Node, StringProperty};
10use fuchsia_inspect_contrib::nodes::BoundedListNode;
11use fuchsia_trace::duration;
12use futures::{TryFutureExt, join, try_join};
13use humansize::{BINARY, FormatSizeOptions, format_size};
14use stalls::StallProvider;
15use traces::CATEGORY_MEMORY_CAPTURE;
16
17use {fidl_fuchsia_kernel as fkernel, fidl_fuchsia_metrics as fmetrics};
18
19/// Periodically collect and report memory attribution data.
20///
21/// This produces a simplified schedule where, instead of having
22/// independent reports on their own cadence, we collect based on
23/// cobalt's frequency and produce all other reports based on that one
24/// collection, saving significant CPU at the expense of flexibility.
25pub async fn periodic_monitoring(
26    kernel_stats_proxy: fkernel::StatsProxy,
27    attribution_data_service: &impl AttributionDataProvider,
28    stall_provider: &impl StallProvider,
29    metric_event_logger: &fmetrics::MetricEventLoggerProxy,
30    bucket_definitions: &[BucketDefinition],
31    inspect_root: Node,
32) -> Result<()> {
33    let mut _current; // Ensure the inspect property is kept as long as necessary.
34    let mut bucket_list_node = std::cell::OnceCell::new();
35    let bucket_names = std::cell::OnceCell::new();
36    let bucket_codes = cobalt::prepare_bucket_codes(bucket_definitions);
37    loop {
38        {
39            duration!(CATEGORY_MEMORY_CAPTURE, c"periodic_monitoring");
40            let timestamp = zx::BootInstant::get();
41            // Retrieve (concurrently) the data necessary to perform the aggregation.
42            let (kmem_stats, kmem_stats_compression) = try_join!(
43                kernel_stats_proxy.get_memory_stats().map_err(anyhow::Error::from),
44                kernel_stats_proxy.get_memory_stats_compression().map_err(anyhow::Error::from)
45            )
46            .with_context(|| "Failed to get kernel memory stats")?;
47            // This is the very expensive operation.
48            let attribution_data = attribute_vmos(attribution_data_service.get_attribution_data()?);
49            let digest = Digest::compute(
50                &attribution_data,
51                &kmem_stats,
52                &kmem_stats_compression,
53                bucket_definitions,
54                false,
55            )?;
56            _current =
57                update_inspect_summary(attribution_data, timestamp, &kmem_stats, &inspect_root);
58            cobalt::upload_metrics(
59                timestamp,
60                &kmem_stats,
61                metric_event_logger,
62                &digest,
63                &bucket_codes,
64            )
65            .await?;
66            {
67                // Initialize the inspect property containing the buckets names, if necessary.
68                let _ = bucket_names.get_or_init(|| {
69                    // Create inspect node to store buckets related information.
70                    let bucket_names =
71                        inspect_root.create_string_array("buckets", digest.buckets.len());
72                    for (i, attribution_processing::digest::Bucket { name, .. }) in
73                        digest.buckets.iter().enumerate()
74                    {
75                        bucket_names.set(i, name);
76                    }
77                    bucket_names
78                });
79            }
80            update_inspect_history(
81                timestamp,
82                &digest,
83                stall_provider,
84                &mut bucket_list_node,
85                &inspect_root,
86            )?;
87        }
88        join!(
89            fuchsia_async::Task::local(async {
90                let _ = scudo::mallopt(scudo::M_PURGE_ALL, 0);
91            }),
92            zx::MonotonicDuration::from_minutes(5).into_timer()
93        );
94    }
95}
96
97fn update_inspect_summary(
98    attribution_data: ProcessedAttributionData,
99    timestamp: zx::BootInstant,
100    kmem_stats: &fkernel::MemoryStats,
101    inspect_root: &Node,
102) -> StringProperty {
103    inspect_root.create_string(
104        "current",
105        record_summary(attribution_data.summary(), timestamp, &kmem_stats),
106    )
107}
108
109/// Update inspect data with collected memory information.
110fn update_inspect_history(
111    timestamp: zx::BootInstant,
112    digest: &Digest,
113    stall_provider: &impl StallProvider,
114    bucket_list_node: &mut std::cell::OnceCell<BoundedListNode>,
115    inspect_root: &Node,
116) -> Result<()> {
117    let stall_values =
118        stall_provider.get_stall_info().with_context(|| "Unable to retrieve stall information")?;
119    // Add an entry for the current aggregation.
120    let _ = bucket_list_node
121        .get_or_init(|| BoundedListNode::new(inspect_root.create_child("measurements"), 100));
122    bucket_list_node.get_mut().unwrap().add_entry(|n| {
123        n.record_int("timestamp", timestamp.into_nanos());
124        {
125            let committed_sizes = n.create_uint_array("bucket_sizes", digest.buckets.len());
126            let populated_sizes =
127                n.create_uint_array("bucket_sizes_populated", digest.buckets.len());
128            for (i, b) in digest.buckets.iter().enumerate() {
129                committed_sizes.set(i, b.committed_size as u64);
130                populated_sizes.set(i, b.populated_size as u64);
131            }
132            n.record(committed_sizes);
133            n.record(populated_sizes);
134        }
135
136        n.record_child("stalls", |child| {
137            child.record_uint(
138                "some_ms",
139                stall_values.some.as_millis().try_into().unwrap_or(u64::MAX),
140            );
141            child.record_uint(
142                "full_ms",
143                stall_values.full.as_millis().try_into().unwrap_or(u64::MAX),
144            );
145        });
146    });
147    Ok(())
148}
149
150fn record_summary(
151    mut summary: MemorySummary,
152    timestamp: zx::Instant<zx::BootTimeline>,
153    kmem_stats: &fkernel::MemoryStats,
154) -> String {
155    let size_options = FormatSizeOptions::from(BINARY).space_after_value(false);
156    summary.principals.sort_by_key(|p| std::cmp::Reverse(p.populated_private));
157    format!(
158        "Time: {} VMO: {} Free: {}\n{}",
159        timestamp.into_nanos(),
160        kmem_stats
161            .vmo_bytes
162            .and_then(|b| Some(format_size(b, size_options)))
163            .unwrap_or_else(|| "?".to_string()),
164        kmem_stats
165            .free_bytes
166            .and_then(|b| Some(format_size(b, size_options)))
167            .unwrap_or_else(|| "?".to_string()),
168        summary
169            .principals
170            .iter_mut()
171            .filter_map(|principal| {
172                if principal.populated_total == 0 {
173                    return None;
174                }
175                let (populated_private, populated_scaled, populated_total) = match (|| {
176                    Some((
177                        format_size(principal.populated_private, size_options),
178                        format_size(principal.populated_scaled as u64, size_options),
179                        format_size(principal.populated_total, size_options),
180                    ))
181                })(
182                ) {
183                    Some(ok) => ok,
184                    None => return None,
185                };
186                let mut vmos = principal.vmos.iter().collect::<Vec<_>>();
187                vmos.sort_by_key(|(_, vmo)| {
188                    std::cmp::Reverse((vmo.committed_private, vmo.committed_scaled as u64))
189                });
190                let sizes = if populated_total == populated_private {
191                    format_args!("{}", populated_total)
192                } else {
193                    format_args!("{} {} {}", populated_private, populated_scaled, populated_total)
194                };
195                Some(format!(
196                    "{}: {}; {}",
197                    principal.name,
198                    sizes,
199                    vmos.iter()
200                        .filter_map(|(name, vmo)| {
201                            if vmo.committed_total == 0 {
202                                None
203                            } else {
204                                Some(format!(
205                                    "{} {} {} {}",
206                                    name,
207                                    format_size(vmo.populated_private, size_options),
208                                    format_size(vmo.populated_scaled as u64, size_options),
209                                    format_size(vmo.populated_total, size_options)
210                                ))
211                            }
212                        })
213                        .collect::<Vec<_>>()
214                        .join("; ")
215                ))
216            })
217            .collect::<Vec<_>>()
218            .join("\n")
219    )
220}
221#[cfg(test)]
222mod tests {
223    use super::*;
224    use attribution_processing::{
225        Attribution, AttributionData, GlobalPrincipalIdentifier, Principal, PrincipalDescription,
226        PrincipalType, Resource, ResourceReference, ZXName,
227    };
228    use diagnostics_assertions::{NonZeroIntProperty, assert_data_tree};
229    use std::num::NonZero;
230    use std::time::Duration;
231
232    use fidl_fuchsia_memory_attribution_plugin as fplugin;
233
234    fn get_kernel_stats() -> (fkernel::MemoryStats, fkernel::MemoryStatsCompression) {
235        (
236            fkernel::MemoryStats {
237                total_bytes: Some(1),
238                free_bytes: Some(2),
239                wired_bytes: Some(3),
240                total_heap_bytes: Some(4),
241                free_heap_bytes: Some(5),
242                vmo_bytes: Some(6),
243                mmu_overhead_bytes: Some(7),
244                ipc_bytes: Some(8),
245                other_bytes: Some(9),
246                free_loaned_bytes: Some(10),
247                cache_bytes: Some(11),
248                slab_bytes: Some(12),
249                zram_bytes: Some(13),
250                vmo_reclaim_total_bytes: Some(14),
251                vmo_reclaim_newest_bytes: Some(15),
252                vmo_reclaim_oldest_bytes: Some(16),
253                vmo_reclaim_disabled_bytes: Some(17),
254                vmo_discardable_locked_bytes: Some(18),
255                vmo_discardable_unlocked_bytes: Some(19),
256                ..Default::default()
257            },
258            fkernel::MemoryStatsCompression {
259                uncompressed_storage_bytes: Some(20),
260                compressed_storage_bytes: Some(21),
261                compressed_fragmentation_bytes: Some(22),
262                compression_time: Some(23),
263                decompression_time: Some(24),
264                total_page_compression_attempts: Some(25),
265                failed_page_compression_attempts: Some(26),
266                total_page_decompressions: Some(27),
267                compressed_page_evictions: Some(28),
268                eager_page_compressions: Some(29),
269                memory_pressure_page_compressions: Some(30),
270                critical_memory_page_compressions: Some(31),
271                pages_decompressed_unit_ns: Some(32),
272                pages_decompressed_within_log_time: Some([40, 41, 42, 43, 44, 45, 46, 47]),
273
274                ..Default::default()
275            },
276        )
277    }
278
279    fn get_attribution_data() -> ProcessedAttributionData {
280        attribute_vmos(AttributionData {
281            principals_vec: vec![Principal {
282                identifier: GlobalPrincipalIdentifier(NonZero::new(1).unwrap()),
283                description: Some(PrincipalDescription::Component("principal".to_owned())),
284                principal_type: PrincipalType::Runnable,
285                parent: None,
286            }],
287            resources_vec: vec![Resource {
288                koid: 10,
289                name_index: 0,
290                resource_type: fplugin::ResourceType::Vmo(fplugin::Vmo {
291                    parent: None,
292                    private_committed_bytes: Some(1024),
293                    private_populated_bytes: Some(2048),
294                    scaled_committed_bytes: Some(1024),
295                    scaled_populated_bytes: Some(2048),
296                    total_committed_bytes: Some(1024),
297                    total_populated_bytes: Some(2048),
298                    ..Default::default()
299                }),
300            }],
301            resource_names: vec![ZXName::from_string_lossy("resource")],
302            attributions: vec![Attribution {
303                source: GlobalPrincipalIdentifier(NonZero::new(1).unwrap()),
304                subject: GlobalPrincipalIdentifier(NonZero::new(1).unwrap()),
305                resources: vec![ResourceReference::KernelObject(10)],
306            }],
307        })
308    }
309
310    #[derive(Clone)]
311    struct FakeStallProvider {}
312    impl StallProvider for FakeStallProvider {
313        fn get_stall_info(&self) -> Result<stalls::MemoryStallMetrics, anyhow::Error> {
314            Ok(stalls::MemoryStallMetrics {
315                some: Duration::from_millis(10),
316                full: Duration::from_millis(20),
317            })
318        }
319    }
320
321    #[fuchsia::test]
322    async fn test_update_inspect() -> Result<()> {
323        let inspector = fuchsia_inspect::Inspector::default();
324        let digest_node = inspector.root().create_child("logger");
325        let timestamp = zx::BootInstant::get();
326        let attribution_data = get_attribution_data();
327        let (kernel_stats, kernel_stats_compression) = get_kernel_stats();
328        let digest = Digest::compute(
329            &attribution_data,
330            &kernel_stats,
331            &kernel_stats_compression,
332            &vec![],
333            false,
334        )?;
335        let mut bucket_list_node = std::cell::OnceCell::new();
336        // Update inspect history twice, and ensure both instances are recorded.
337        let _summary =
338            update_inspect_summary(attribution_data, timestamp, &kernel_stats, &digest_node);
339        update_inspect_history(
340            timestamp,
341            &digest,
342            &FakeStallProvider {},
343            &mut bucket_list_node,
344            &digest_node,
345        )?;
346
347        update_inspect_history(
348            timestamp,
349            &digest,
350            &FakeStallProvider {},
351            &mut bucket_list_node,
352            &digest_node,
353        )?;
354        assert_data_tree!(inspector, root: {
355            logger: {
356                measurements: {
357                    // First update.
358                    "0": {
359                        timestamp: NonZeroIntProperty,
360                        bucket_sizes: vec![
361                            1024u64, // Undigested: matches the single unmatched VMO
362                            // Orphaned: vmo_bytes reported by the kernel but not covered by any
363                            // bucket => 6 - 1024 => 0 (saturating, cannot be negative)
364                            0u64,
365                            31u64,   // Kernel: 3 wired + 4 heap + 7 mmu + 8 IPC + 9 other = 31
366                            2u64,    // Free
367                            14u64,   // [Addl]PagerTotal
368                            15u64,   // [Addl]PagerNewest
369                            16u64,   // [Addl]PagerOldest
370                            18u64,   // [Addl]DiscardableLocked
371                            19u64,   // [Addl]DiscardableUnlocked
372                            21u64,   // [Addl]ZramCompressedBytes
373                        ],
374                        bucket_sizes_populated: vec![
375                            2048u64, // Undigested: matches the single unmatched VMO
376                            // Orphaned: vmo_bytes reported by the kernel but not covered by any
377                            // bucket => 6 - 1024 => 0 (saturating, cannot be negative)
378                            0u64,
379                            31u64,   // Kernel: 3 wired + 4 heap + 7 mmu + 8 IPC + 9 other = 31
380                            2u64,    // Free
381                            14u64,   // [Addl]PagerTotal
382                            15u64,   // [Addl]PagerNewest
383                            16u64,   // [Addl]PagerOldest
384                            18u64,   // [Addl]DiscardableLocked
385                            19u64,   // [Addl]DiscardableUnlocked
386                            21u64,   // [Addl]ZramCompressedBytes
387                        ],
388
389                        stalls: {
390                            some_ms: 10u64,
391                            full_ms: 20u64,
392                        },
393                    },
394                    // Second update.
395                    "1": {
396                        timestamp: NonZeroIntProperty,
397                        bucket_sizes: vec![
398                            1024u64, // Undigested: matches the single unmatched VMO
399                            // Orphaned: vmo_bytes reported by the kernel but not covered by any
400                            // bucket => 6 - 1024 => 0 (saturating, cannot be negative)
401                            0u64,
402                            31u64,   // Kernel: 3 wired + 4 heap + 7 mmu + 8 IPC + 9 other = 31
403                            2u64,    // Free
404                            14u64,   // [Addl]PagerTotal
405                            15u64,   // [Addl]PagerNewest
406                            16u64,   // [Addl]PagerOldest
407                            18u64,   // [Addl]DiscardableLocked
408                            19u64,   // [Addl]DiscardableUnlocked
409                            21u64,   // [Addl]ZramCompressedBytes
410                        ],
411                        bucket_sizes_populated: vec![
412                            2048u64, // Undigested: matches the single unmatched VMO
413                            // Orphaned: vmo_bytes reported by the kernel but not covered by any
414                            // bucket => 6 - 1024 => 0 (saturating, cannot be negative)
415                            0u64,
416                            31u64,   // Kernel: 3 wired + 4 heap + 7 mmu + 8 IPC + 9 other = 31
417                            2u64,    // Free
418                            14u64,   // [Addl]PagerTotal
419                            15u64,   // [Addl]PagerNewest
420                            16u64,   // [Addl]PagerOldest
421                            18u64,   // [Addl]DiscardableLocked
422                            19u64,   // [Addl]DiscardableUnlocked
423                            21u64,   // [Addl]ZramCompressedBytes
424                        ],
425                        stalls: {
426                            some_ms: 10u64,
427                            full_ms: 20u64,
428                        },
429                    },
430                },
431                current: regex::Regex::new(r"^Time: \d+ VMO: 6B Free: 2B\nprincipal: 2KiB; resource 2KiB 2KiB 2KiB")?,
432            },
433        });
434        Ok(())
435    }
436}