1use anyhow::{Error, format_err};
6use fuchsia_inspect::reader::snapshot::ScannedBlock;
7use inspect_format::{Array, BlockType, Buffer, Extent, Name, PropertyFormat, StringRef, Unknown};
8use serde::Serialize;
9use std::collections::HashMap;
10
11const NUMERIC_TYPE_SIZE: usize = 8;
26
27#[derive(Debug)]
29pub struct BlockMetrics {
30 description: String,
31 header_bytes: usize,
32 data_bytes: usize,
33 total_bytes: usize,
34}
35
36impl BlockMetrics {
37 pub fn set_data_bytes(&mut self, bytes: usize) {
38 self.data_bytes = bytes;
39 }
40
41 #[cfg(test)]
42 pub(crate) fn sample_for_test(
43 description: String,
44 header_bytes: usize,
45 data_bytes: usize,
46 total_bytes: usize,
47 ) -> BlockMetrics {
48 BlockMetrics { description, header_bytes, data_bytes, total_bytes }
49 }
50}
51
52#[derive(PartialEq)]
54pub(crate) enum BlockStatus {
55 Used,
56 NotUsed,
57}
58
59#[derive(Debug, Default, Serialize, PartialEq)]
61pub struct BlockStatistics {
62 pub count: u64,
63 pub header_bytes: usize,
64 pub data_bytes: usize,
65 pub total_bytes: usize,
66 pub data_percent: u64,
67}
68
69impl BlockStatistics {
70 fn update(&mut self, numbers: &BlockMetrics, status: BlockStatus) {
71 let BlockMetrics { header_bytes, data_bytes, total_bytes, .. } = numbers;
72 self.header_bytes += header_bytes;
73 if status == BlockStatus::Used {
74 self.data_bytes += data_bytes;
75 }
76 self.total_bytes += total_bytes;
77 if self.total_bytes > 0 {
78 self.data_percent = (self.data_bytes * 100 / self.total_bytes) as u64;
79 }
80 }
81}
82
83#[derive(Debug, Serialize, Default)]
85pub struct Metrics {
86 pub block_count: u64,
87 pub size: usize,
88 pub block_statistics: HashMap<String, BlockStatistics>,
89}
90
91impl Metrics {
92 pub fn new() -> Metrics {
93 Self::default()
94 }
95
96 pub(crate) fn record(&mut self, metrics: &BlockMetrics, status: BlockStatus) {
97 let description = match status {
98 BlockStatus::NotUsed => format!("{}(UNUSED)", metrics.description),
99 BlockStatus::Used => metrics.description.clone(),
100 };
101 let statistics = self.block_statistics.entry(description).or_default();
102 statistics.count += 1;
103 statistics.update(metrics, status);
104 self.block_count += 1;
105 self.size += metrics.total_bytes;
106 }
107
108 pub fn process(&mut self, block: ScannedBlock<'_, Unknown>) -> Result<(), Error> {
111 self.record(&Metrics::analyze(block)?, BlockStatus::Used);
112 Ok(())
113 }
114
115 pub fn analyze(block: ScannedBlock<'_, Unknown>) -> Result<BlockMetrics, Error> {
116 let block_type = block.block_type().ok_or_else(|| format_err!("Missing block type"))?;
117 let mut description = None;
118
119 let total_bytes = inspect_format::constants::MIN_ORDER_SIZE << block.order();
120 let data_bytes = match block_type {
121 BlockType::Header => 4,
122 BlockType::Reserved
123 | BlockType::NodeValue
124 | BlockType::Free
125 | BlockType::Tombstone
126 | BlockType::LinkValue => 0,
127 BlockType::IntValue
128 | BlockType::UintValue
129 | BlockType::DoubleValue
130 | BlockType::BoolValue => NUMERIC_TYPE_SIZE,
131 BlockType::BufferValue => {
132 let block = block.cast::<Buffer>().unwrap();
133 description = Some(
134 match block.format().ok_or_else(|| format_err!("Missing property format"))? {
135 PropertyFormat::String => "STRING".to_owned(),
136 PropertyFormat::Bytes => "BYTES".to_owned(),
137 PropertyFormat::StringReference => "STRING_REFERENCE".to_owned(),
138 },
139 );
140 0
141 }
142 BlockType::ArrayValue => {
143 let block = block.cast::<Array<Unknown>>().unwrap();
144 let entry_type = block.entry_type().ok_or_else(|| format_err!("format missing"))?;
145 let format = block.format().ok_or_else(|| format_err!("format missing"))?;
146 description = Some(format!("ARRAY({format:?}, {entry_type})"));
147 block.entry_type_size().ok_or_else(|| format_err!("entry type must be sized"))?
148 * block.slots()
149 }
150 BlockType::Name => {
151 let block = block.cast::<Name>().unwrap();
152 block.length()
153 }
154 BlockType::Extent => {
155 let block = block.cast::<Extent>().unwrap();
156 block.contents()?.len()
157 }
158 BlockType::StringReference => {
159 let block = block.cast::<StringRef>().unwrap();
160 block.total_length()
161 }
162 };
163
164 let header_bytes = match block_type {
165 BlockType::Header
166 | BlockType::NodeValue
167 | BlockType::BufferValue
168 | BlockType::Free
169 | BlockType::Reserved
170 | BlockType::Tombstone
171 | BlockType::ArrayValue
172 | BlockType::LinkValue => 16,
173 BlockType::StringReference => 12,
174 BlockType::IntValue
175 | BlockType::DoubleValue
176 | BlockType::UintValue
177 | BlockType::BoolValue
178 | BlockType::Name
179 | BlockType::Extent => 8,
180 };
181
182 Ok(BlockMetrics {
183 description: description.unwrap_or_else(|| format!("{block_type}")),
184 data_bytes,
185 header_bytes,
186 total_bytes,
187 })
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194 use crate::results::Results;
195 use crate::{data, puppet};
196 use anyhow::{bail, format_err};
197 use inspect_format::{
198 ArrayFormat, BlockAccessorMutExt, BlockIndex, HeaderFields, PayloadFields, constants,
199 };
200
201 #[fuchsia::test]
202 async fn metrics_work() -> Result<(), Error> {
203 let puppet = puppet::tests::local_incomplete_puppet().await?;
204 let metrics = puppet.metrics().unwrap();
205 let mut results = Results::new();
206 results.remember_metrics(metrics, "trialfoo", 42, "stepfoo");
207 let json = results.to_json();
208 assert!(json.contains("\"trial_name\":\"trialfoo\""), "{}", json);
209 assert!(json.contains(&format!("\"size\":{}", puppet::VMO_SIZE)), "{}", json);
210 assert!(json.contains("\"step_index\":42"), "{}", json);
211 assert!(json.contains("\"step_name\":\"stepfoo\""), "{}", json);
212 assert!(json.contains("\"block_count\":8"), "{}", json);
213 assert!(json.contains("\"HEADER\":{\"count\":1,\"header_bytes\":16,\"data_bytes\":4,\"total_bytes\":32,\"data_percent\":12}"), "{}", json);
214 Ok(())
215 }
216
217 #[allow(clippy::too_many_arguments)]
218 #[track_caller]
219 fn test_metrics(
220 buffer: &[u8],
221 block_count: u64,
222 size: usize,
223 description: &str,
224 correct_statistics: BlockStatistics,
225 ) -> Result<(), Error> {
226 let metrics = data::Scanner::try_from(buffer).map(|d| d.metrics())?;
227 assert_eq!(metrics.block_count, block_count, "Bad block_count for {description}");
228 assert_eq!(metrics.size, size, "Bad size for {description}");
229 match metrics.block_statistics.get(description) {
230 None => {
231 return Err(format_err!(
232 "block {} not found in {:?}",
233 description,
234 metrics.block_statistics.keys()
235 ));
236 }
237 Some(statistics) if statistics == &correct_statistics => {}
238 Some(unexpected) => bail!(
239 "Value mismatch, {:?} vs {:?} for {}",
240 unexpected,
241 correct_statistics,
242 description
243 ),
244 }
245 Ok(())
246 }
247
248 fn copy_into(source: &[u8], dest: &mut [u8], index: usize, offset: usize) {
249 let offset = index * 16 + offset;
250 dest[offset..offset + source.len()].copy_from_slice(source);
251 }
252
253 macro_rules! put_header {
254 ($block:ident, $index:expr, $buffer:expr) => {
255 copy_into(&HeaderFields::value(&$block).to_le_bytes(), $buffer, $index, 0);
256 };
257 }
258 macro_rules! put_payload {
259 ($block:ident, $index:expr, $buffer:expr) => {
260 copy_into(&PayloadFields::value(&$block).to_le_bytes(), $buffer, $index, 8);
261 };
262 }
263 macro_rules! set_type {
264 ($block:ident, $block_type:ident) => {
265 HeaderFields::set_block_type(&mut $block, BlockType::$block_type as u8)
266 };
267 }
268
269 const NAME_INDEX: u32 = 3;
270
271 fn init_vmo_contents(mut buffer: &mut [u8]) {
275 const HEADER_INDEX: usize = 0;
276
277 let mut container = [0u8; 16];
278 let mut header = container.block_at_mut(BlockIndex::EMPTY);
279 HeaderFields::set_order(&mut header, constants::HEADER_ORDER);
280 set_type!(header, Header);
281 HeaderFields::set_header_magic(&mut header, constants::HEADER_MAGIC_NUMBER);
282 HeaderFields::set_header_version(&mut header, constants::HEADER_VERSION_NUMBER);
283 put_header!(header, HEADER_INDEX, &mut buffer);
284 let mut container = [0u8; 16];
285 let mut name_header = container.block_at_mut(BlockIndex::EMPTY);
286 set_type!(name_header, Name);
287 HeaderFields::set_name_length(&mut name_header, 4);
288 put_header!(name_header, NAME_INDEX as usize, &mut buffer);
289 }
290
291 #[fuchsia::test]
292 fn header_metrics() -> Result<(), Error> {
293 let mut buffer = [0u8; 256];
294 init_vmo_contents(&mut buffer);
295 test_metrics(
296 &buffer,
297 15,
298 256,
299 "HEADER",
300 BlockStatistics {
301 count: 1,
302 header_bytes: 16,
303 data_bytes: 4,
304 total_bytes: 32,
305 data_percent: 12,
306 },
307 )?;
308 test_metrics(
309 &buffer,
310 15,
311 256,
312 "FREE",
313 BlockStatistics {
314 count: 13,
315 header_bytes: 208,
316 data_bytes: 0,
317 total_bytes: 208,
318 data_percent: 0,
319 },
320 )?;
321 test_metrics(
322 &buffer,
323 15,
324 256,
325 "NAME(UNUSED)",
326 BlockStatistics {
327 count: 1,
328 header_bytes: 8,
329 data_bytes: 0,
330 total_bytes: 16,
331 data_percent: 0,
332 },
333 )?;
334 Ok(())
335 }
336
337 #[fuchsia::test]
338 fn reserved_metrics() -> Result<(), Error> {
339 let mut buffer = [0u8; 256];
340 init_vmo_contents(&mut buffer);
341 let mut container = [0u8; 16];
342 let mut reserved_header = container.block_at_mut(BlockIndex::EMPTY);
343 set_type!(reserved_header, Reserved);
344 HeaderFields::set_order(&mut reserved_header, 1);
345 put_header!(reserved_header, 2, &mut buffer);
346 test_metrics(
347 &buffer,
348 14,
349 256,
350 "RESERVED",
351 BlockStatistics {
352 count: 1,
353 header_bytes: 16,
354 data_bytes: 0,
355 total_bytes: 32,
356 data_percent: 0,
357 },
358 )?;
359 Ok(())
360 }
361
362 #[fuchsia::test]
363 fn node_metrics() -> Result<(), Error> {
364 let mut buffer = [0u8; 256];
365 init_vmo_contents(&mut buffer);
366 let mut container = [0u8; 16];
367 let mut node_header = container.block_at_mut(BlockIndex::EMPTY);
368 set_type!(node_header, NodeValue);
369 HeaderFields::set_value_parent_index(&mut node_header, 1);
370 put_header!(node_header, 2, &mut buffer);
371 test_metrics(
372 &buffer,
373 15,
374 256,
375 "NODE_VALUE(UNUSED)",
376 BlockStatistics {
377 count: 1,
378 header_bytes: 16,
379 data_bytes: 0,
380 total_bytes: 16,
381 data_percent: 0,
382 },
383 )?;
384 HeaderFields::set_value_name_index(&mut node_header, NAME_INDEX);
385 HeaderFields::set_value_parent_index(&mut node_header, 0);
386 put_header!(node_header, 2, &mut buffer);
387 test_metrics(
388 &buffer,
389 15,
390 256,
391 "NODE_VALUE",
392 BlockStatistics {
393 count: 1,
394 header_bytes: 16,
395 data_bytes: 0,
396 total_bytes: 16,
397 data_percent: 0,
398 },
399 )?;
400 test_metrics(
401 &buffer,
402 15,
403 256,
404 "NAME",
405 BlockStatistics {
406 count: 1,
407 header_bytes: 8,
408 data_bytes: 4,
409 total_bytes: 16,
410 data_percent: 25,
411 },
412 )?;
413 set_type!(node_header, Tombstone);
414 put_header!(node_header, 2, &mut buffer);
415 test_metrics(
416 &buffer,
417 15,
418 256,
419 "TOMBSTONE",
420 BlockStatistics {
421 count: 1,
422 header_bytes: 16,
423 data_bytes: 0,
424 total_bytes: 16,
425 data_percent: 0,
426 },
427 )?;
428 test_metrics(
429 &buffer,
430 15,
431 256,
432 "NAME(UNUSED)",
433 BlockStatistics {
434 count: 1,
435 header_bytes: 8,
436 data_bytes: 0,
437 total_bytes: 16,
438 data_percent: 0,
439 },
440 )?;
441 Ok(())
442 }
443
444 #[fuchsia::test]
445 fn number_metrics() -> Result<(), Error> {
446 let mut buffer = [0u8; 256];
447 init_vmo_contents(&mut buffer);
448 macro_rules! test_number {
449 ($number_type:ident, $parent:expr, $block_string:expr, $data_size:expr, $data_percent:expr) => {
450 let mut container = [0u8; 16];
451 let mut value = container.block_at_mut(BlockIndex::EMPTY);
452 set_type!(value, $number_type);
453 HeaderFields::set_value_name_index(&mut value, NAME_INDEX);
454 HeaderFields::set_value_parent_index(&mut value, $parent);
455 put_header!(value, 2, &mut buffer);
456 test_metrics(
457 &buffer,
458 15,
459 256,
460 $block_string,
461 BlockStatistics {
462 count: 1,
463 header_bytes: 8,
464 data_bytes: $data_size,
465 total_bytes: 16,
466 data_percent: $data_percent,
467 },
468 )?;
469 };
470 }
471 test_number!(IntValue, 0, "INT_VALUE", 8, 50);
472 test_number!(IntValue, 5, "INT_VALUE(UNUSED)", 0, 0);
473 test_number!(DoubleValue, 0, "DOUBLE_VALUE", 8, 50);
474 test_number!(DoubleValue, 5, "DOUBLE_VALUE(UNUSED)", 0, 0);
475 test_number!(UintValue, 0, "UINT_VALUE", 8, 50);
476 test_number!(UintValue, 5, "UINT_VALUE(UNUSED)", 0, 0);
477 Ok(())
478 }
479
480 #[fuchsia::test]
481 fn property_metrics() -> Result<(), Error> {
482 let mut buffer = [0u8; 256];
483 init_vmo_contents(&mut buffer);
484 let mut container = [0u8; 16];
485 let mut value = container.block_at_mut(BlockIndex::EMPTY);
486 set_type!(value, BufferValue);
487 HeaderFields::set_value_name_index(&mut value, NAME_INDEX);
488 HeaderFields::set_value_parent_index(&mut value, 0);
489 put_header!(value, 2, &mut buffer);
490 PayloadFields::set_property_total_length(&mut value, 12);
491 PayloadFields::set_property_extent_index(&mut value, 4);
492 PayloadFields::set_property_flags(&mut value, PropertyFormat::String as u8);
493 put_payload!(value, 2, &mut buffer);
494 let mut container = [0u8; 16];
495 let mut extent = container.block_at_mut(BlockIndex::EMPTY);
496 set_type!(extent, Extent);
497 HeaderFields::set_extent_next_index(&mut extent, 5);
498 put_header!(extent, 4, &mut buffer);
499 HeaderFields::set_extent_next_index(&mut extent, 0);
500 put_header!(extent, 5, &mut buffer);
501 test_metrics(
502 &buffer,
503 15,
504 256,
505 "EXTENT",
506 BlockStatistics {
507 count: 2,
508 header_bytes: 16,
509 data_bytes: 12,
510 total_bytes: 32,
511 data_percent: 37,
512 },
513 )?;
514 test_metrics(
515 &buffer,
516 15,
517 256,
518 "STRING",
519 BlockStatistics {
520 count: 1,
521 header_bytes: 16,
522 data_bytes: 0,
523 total_bytes: 16,
524 data_percent: 0,
525 },
526 )?;
527 PayloadFields::set_property_flags(&mut value, PropertyFormat::Bytes as u8);
528 put_payload!(value, 2, &mut buffer);
529 test_metrics(
530 &buffer,
531 15,
532 256,
533 "EXTENT",
534 BlockStatistics {
535 count: 2,
536 header_bytes: 16,
537 data_bytes: 12,
538 total_bytes: 32,
539 data_percent: 37,
540 },
541 )?;
542 test_metrics(
543 &buffer,
544 15,
545 256,
546 "BYTES",
547 BlockStatistics {
548 count: 1,
549 header_bytes: 16,
550 data_bytes: 0,
551 total_bytes: 16,
552 data_percent: 0,
553 },
554 )?;
555 HeaderFields::set_value_parent_index(&mut value, 7);
556 put_header!(value, 2, &mut buffer);
557 test_metrics(
558 &buffer,
559 15,
560 256,
561 "EXTENT(UNUSED)",
562 BlockStatistics {
563 count: 2,
564 header_bytes: 16,
565 data_bytes: 0,
566 total_bytes: 32,
567 data_percent: 0,
568 },
569 )?;
570 test_metrics(
571 &buffer,
572 15,
573 256,
574 "BYTES(UNUSED)",
575 BlockStatistics {
576 count: 1,
577 header_bytes: 16,
578 data_bytes: 0,
579 total_bytes: 16,
580 data_percent: 0,
581 },
582 )?;
583 Ok(())
584 }
585
586 #[fuchsia::test]
587 fn array_metrics() {
588 let mut buffer = [0u8; 256];
589 init_vmo_contents(&mut buffer);
590 let mut container = [0u8; 16];
591 let mut value = container.block_at_mut(BlockIndex::EMPTY);
592
593 set_type!(value, ArrayValue);
594 HeaderFields::set_order(&mut value, 3);
595 HeaderFields::set_value_name_index(&mut value, NAME_INDEX);
596 HeaderFields::set_value_parent_index(&mut value, 0);
597 put_header!(value, 4, &mut buffer);
598 PayloadFields::set_array_entry_type(&mut value, BlockType::IntValue as u8);
599 PayloadFields::set_array_flags(&mut value, ArrayFormat::Default as u8);
600 PayloadFields::set_array_slots_count(&mut value, 4);
601 put_payload!(value, 4, &mut buffer);
602 test_metrics(
603 &buffer,
604 8,
605 256,
606 "ARRAY(Default, INT_VALUE)",
607 BlockStatistics {
608 count: 1,
609 header_bytes: 16,
610 data_bytes: 32,
611 total_bytes: 128,
612 data_percent: 25,
613 },
614 )
615 .unwrap();
616
617 PayloadFields::set_array_entry_type(&mut value, BlockType::StringReference as u8);
618 PayloadFields::set_array_flags(&mut value, ArrayFormat::Default as u8);
619 PayloadFields::set_array_slots_count(&mut value, 4);
620 put_payload!(value, 4, &mut buffer);
621 test_metrics(
622 &buffer,
623 8,
624 256,
625 "ARRAY(Default, STRING_REFERENCE)",
626 BlockStatistics {
627 count: 1,
628 header_bytes: 16,
629 data_bytes: 16,
630 total_bytes: 128,
631 data_percent: 12,
632 },
633 )
634 .unwrap();
635
636 PayloadFields::set_array_flags(&mut value, ArrayFormat::LinearHistogram as u8);
637 PayloadFields::set_array_entry_type(&mut value, BlockType::IntValue as u8);
638 PayloadFields::set_array_slots_count(&mut value, 8);
639 put_payload!(value, 4, &mut buffer);
640 test_metrics(
641 &buffer,
642 8,
643 256,
644 "ARRAY(LinearHistogram, INT_VALUE)",
645 BlockStatistics {
646 count: 1,
647 header_bytes: 16,
648 data_bytes: 64,
649 total_bytes: 128,
650 data_percent: 50,
651 },
652 )
653 .unwrap();
654
655 PayloadFields::set_array_flags(&mut value, ArrayFormat::ExponentialHistogram as u8);
656 let name = "ARRAY(ExponentialHistogram, INT_VALUE)";
658 put_payload!(value, 4, &mut buffer);
659 test_metrics(
660 &buffer,
661 8,
662 256,
663 name,
664 BlockStatistics {
665 count: 1,
666 header_bytes: 16,
667 data_bytes: 64,
668 total_bytes: 128,
669 data_percent: 50,
670 },
671 )
672 .unwrap();
673
674 PayloadFields::set_array_entry_type(&mut value, BlockType::UintValue as u8);
675 let name = "ARRAY(ExponentialHistogram, UINT_VALUE)";
676 put_payload!(value, 4, &mut buffer);
677 test_metrics(
678 &buffer,
679 8,
680 256,
681 name,
682 BlockStatistics {
683 count: 1,
684 header_bytes: 16,
685 data_bytes: 64,
686 total_bytes: 128,
687 data_percent: 50,
688 },
689 )
690 .unwrap();
691
692 PayloadFields::set_array_entry_type(&mut value, BlockType::DoubleValue as u8);
693 let name = "ARRAY(ExponentialHistogram, DOUBLE_VALUE)";
694 put_payload!(value, 4, &mut buffer);
695 test_metrics(
696 &buffer,
697 8,
698 256,
699 name,
700 BlockStatistics {
701 count: 1,
702 header_bytes: 16,
703 data_bytes: 64,
704 total_bytes: 128,
705 data_percent: 50,
706 },
707 )
708 .unwrap();
709
710 HeaderFields::set_value_parent_index(&mut value, 1);
711 let name = "ARRAY(ExponentialHistogram, DOUBLE_VALUE)(UNUSED)";
712 put_header!(value, 4, &mut buffer);
713 test_metrics(
714 &buffer,
715 8,
716 256,
717 name,
718 BlockStatistics {
719 count: 1,
720 header_bytes: 16,
721 data_bytes: 0,
722 total_bytes: 128,
723 data_percent: 0,
724 },
725 )
726 .unwrap();
727 }
728}