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