1use 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
21pub 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; 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 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 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 let _ = bucket_names.get_or_init(|| {
72 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
113fn 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 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 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 "0": {
382 timestamp: NonZeroIntProperty,
383 bucket_sizes: vec![
384 1024u64, 0u64,
388 54u64, 2u64, 14u64, 15u64, 16u64, 18u64, 19u64, 21u64, 6u64, ],
398 bucket_sizes_populated: vec![
399 2048u64, 0u64,
403 54u64, 2u64, 14u64, 15u64, 16u64, 18u64, 19u64, 21u64, 6u64, ],
413
414 stalls: {
415 some_ms: 10u64,
416 full_ms: 20u64,
417 },
418 refaults: {
419 count: 300u64,
420 },
421 },
422 "1": {
424 timestamp: NonZeroIntProperty,
425 bucket_sizes: vec![
426 1024u64, 0u64,
430 54u64, 2u64, 14u64, 15u64, 16u64, 18u64, 19u64, 21u64, 6u64, ],
440 bucket_sizes_populated: vec![
441 2048u64, 0u64,
445 54u64, 2u64, 14u64, 15u64, 16u64, 18u64, 19u64, 21u64, 6u64, ],
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}