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 traces::CATEGORY_MEMORY_CAPTURE;
16
17use {fidl_fuchsia_kernel as fkernel, fidl_fuchsia_metrics as fmetrics};
18
19pub 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; 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 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 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 let _ = bucket_names.get_or_init(|| {
69 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
109fn 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 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 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 "0": {
359 timestamp: NonZeroIntProperty,
360 bucket_sizes: vec![
361 1024u64, 0u64,
365 31u64, 2u64, 14u64, 15u64, 16u64, 18u64, 19u64, 21u64, ],
374 bucket_sizes_populated: vec![
375 2048u64, 0u64,
379 31u64, 2u64, 14u64, 15u64, 16u64, 18u64, 19u64, 21u64, ],
388
389 stalls: {
390 some_ms: 10u64,
391 full_ms: 20u64,
392 },
393 },
394 "1": {
396 timestamp: NonZeroIntProperty,
397 bucket_sizes: vec![
398 1024u64, 0u64,
402 31u64, 2u64, 14u64, 15u64, 16u64, 18u64, 19u64, 21u64, ],
411 bucket_sizes_populated: vec![
412 2048u64, 0u64,
416 31u64, 2u64, 14u64, 15u64, 16u64, 18u64, 19u64, 21u64, ],
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}