inspect_validator/
metrics.rs

1// Copyright 2019 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.
4
5use 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
11// Blocks such as Node, Extent, and Name may or may not be part of the Inspect tree. We want to
12// count each case separately. Also, Extent blocks can't be fully analyzed when first scanned,
13// since they don't store their own data size. So there's a three-step process to gather metrics.
14// All these take place while Scanner is reading the VMO:
15//
16// 1) Analyze each block in the VMO with Metrics::analyze().
17// 2) While building the tree,
18//   2A) set the data size for Extent blocks;
19//   2B) record the metrics for all blocks in the tree.
20// 3) Record metrics as "NotUsed" for all remaining blocks, first setting their data size to 0.
21//
22// This can be combined for blocks that are never part of the tree, like Free and Reserved blocks.
23
24// How many bytes are used to store a single number (same for i, u, and f, defined in the VMO spec)
25const NUMERIC_TYPE_SIZE: usize = 8;
26
27// Metrics for an individual block - will be remembered alongside the block's data by the Scanner.
28#[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// Tells whether the block was used in the Inspect tree or not.
53#[derive(PartialEq)]
54pub(crate) enum BlockStatus {
55    Used,
56    NotUsed,
57}
58
59// Gathers statistics for a type of block.
60#[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// Stores statistics for every type (description) of block, plus VMO as a whole.
84#[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    // Process (in a single operation) a block of a type that will never be part of the Inspect
109    // data tree.
110    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    // Creates the required Header block. Also creates a Name block because
272    // lots of things use it.
273    // Note that \0 is a valid UTF-8 character so there's no need to set string data.
274    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        // avoid line-wrapping the parameter list of test_metrics()
657        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}