diff --git a/Cargo.toml b/Cargo.toml index 3364650..0bd421a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ keywords = ["no_std", "embedded", "flash", "storage"] embedded-storage-async = "0.4.1" defmt = { version = "0.3", optional = true } futures = { version = "0.3.30", features = ["executor"], optional = true } +approx = { version = "0.5.1", optional = true } [dev-dependencies] approx = "0.5.1" @@ -23,4 +24,4 @@ futures-test = "0.3.30" [features] defmt = ["dep:defmt"] -_test = ["dep:futures"] +_test = ["dep:futures", "dep:approx"] diff --git a/README.md b/README.md index d4ff13c..1fdac2e 100644 --- a/README.md +++ b/README.md @@ -73,12 +73,16 @@ Instead, we can optionally store some state in ram. These numbers are taken from the test cases in the cache module: -| Name | Map # flash reads | Queue # flash reads | -| -------------: | ----------------: | ------------------: | -| NoCache | 100% | 100% | -| PageStateCache | 77% | 51% | +| Name | Map # flash reads | Map flash bytes read | Queue # flash reads | Queue flash bytes read | +| -------------: | ----------------: | -------------------: | ------------------: | ---------------------: | +| NoCache | 100% | 100% | 100% | 100% | +| PageStateCache | 77% | 97% | 51% | 90% | -***Note:** These are the number of reads, not the amount of bytes.* +#### Takeaways + +- PageStateCache + - Mostly tackles number of reads + - Very cheap in RAM, so easy win ## Inner workings diff --git a/src/cache.rs b/src/cache.rs index ee31aae..e65140a 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -177,7 +177,7 @@ mod queue_tests { use core::ops::Range; use crate::{ - mock_flash::{self, WriteCountCheck}, + mock_flash::{self, FlashStatsResult, WriteCountCheck}, queue::{peek, pop, push}, AlignedBuf, }; @@ -190,23 +190,40 @@ mod queue_tests { #[test] async fn no_cache() { - assert_eq!(run_test(&mut NoCache::new()).await, (594934, 6299, 146)); + assert_eq!( + run_test(&mut NoCache::new()).await, + FlashStatsResult { + erases: 146, + reads: 594934, + writes: 6299, + bytes_read: 2766058, + bytes_written: 53299 + } + ); } #[test] async fn page_state_cache() { assert_eq!( run_test(&mut PageStateCache::::new()).await, - (308740, 6299, 146) + FlashStatsResult { + erases: 146, + reads: 308740, + writes: 6299, + bytes_read: 2479864, + bytes_written: 53299 + } ); } - async fn run_test(mut cache: impl CacheImpl) -> (u32, u32, u32) { + async fn run_test(mut cache: impl CacheImpl) -> FlashStatsResult { let mut flash = mock_flash::MockFlashBase::::new(WriteCountCheck::Twice, None, true); const FLASH_RANGE: Range = 0x00..0x400; let mut data_buffer = AlignedBuf([0; 1024]); + let start_snapshot = flash.stats_snapshot(); + for i in 0..LOOP_COUNT { println!("{i}"); let data = vec![i as u8; i % 20 + 1]; @@ -243,7 +260,7 @@ mod queue_tests { println!("DONE"); } - (flash.reads, flash.writes, flash.erases) + start_snapshot.compare_to(flash.stats_snapshot()) } } @@ -253,7 +270,7 @@ mod map_tests { use crate::{ map::{fetch_item, store_item, StorageItem}, - mock_flash::{self, WriteCountCheck}, + mock_flash::{self, FlashStatsResult, WriteCountCheck}, AlignedBuf, }; @@ -264,14 +281,29 @@ mod map_tests { #[test] async fn no_cache() { - assert_eq!(run_test(&mut NoCache::new()).await, (224161, 5201, 198)); + assert_eq!( + run_test(&mut NoCache::new()).await, + FlashStatsResult { + erases: 198, + reads: 224161, + writes: 5201, + bytes_read: 1770974, + bytes_written: 50401 + } + ); } #[test] async fn page_state_cache() { assert_eq!( run_test(&mut PageStateCache::::new()).await, - (172831, 5201, 198) + FlashStatsResult { + erases: 198, + reads: 172831, + writes: 5201, + bytes_read: 1719644, + bytes_written: 50401 + } ); } @@ -343,7 +375,7 @@ mod map_tests { } } - async fn run_test(mut cache: impl CacheImpl) -> (u32, u32, u32) { + async fn run_test(mut cache: impl CacheImpl) -> FlashStatsResult { let mut cache = cache.inner(); let mut flash = @@ -355,6 +387,8 @@ mod map_tests { 11, 13, 6, 13, 13, 10, 2, 3, 5, 36, 1, 65, 4, 6, 1, 15, 10, 7, 3, 15, 9, 3, 4, 5, ]; + let start_snapshot = flash.stats_snapshot(); + for _ in 0..100 { for i in 0..24 { let item = MockStorageItem { @@ -385,6 +419,6 @@ mod map_tests { } } - (flash.reads, flash.writes, flash.erases) + start_snapshot.compare_to(flash.stats_snapshot()) } } diff --git a/src/map.rs b/src/map.rs index 117fb74..67aeb23 100644 --- a/src/map.rs +++ b/src/map.rs @@ -730,6 +730,8 @@ mod tests { let mut data_buffer = AlignedBuf([0; 128]); + let start_snapshot = flash.stats_snapshot(); + let item = fetch_item::( &mut flash, flash_range.clone(), @@ -900,10 +902,7 @@ mod tests { assert_eq!(item.value, vec![(i % 10) as u8 * 2; (i % 10) as usize]); } - println!( - "Erases: {}, reads: {}, writes: {}", - flash.erases, flash.reads, flash.writes - ); + println!("{:?}", start_snapshot.compare_to(flash.stats_snapshot()),); } #[test] diff --git a/src/mock_flash.rs b/src/mock_flash.rs index a711081..d036548 100644 --- a/src/mock_flash.rs +++ b/src/mock_flash.rs @@ -1,4 +1,4 @@ -use core::ops::Range; +use core::ops::{Add, AddAssign, Range}; use embedded_storage_async::nor_flash::{ ErrorType, MultiwriteNorFlash, NorFlash, NorFlashError, NorFlashErrorKind, ReadNorFlash, }; @@ -21,12 +21,7 @@ use Writable::*; pub struct MockFlashBase { writable: Vec, data: Vec, - /// Number of erases done. - pub erases: u32, - /// Number of reads done. - pub reads: u32, - /// Number of writes done. - pub writes: u32, + current_stats: FlashStatsSnapshot, /// Check that all write locations are writeable. pub write_count_check: WriteCountCheck, /// A countdown to shutoff. When some and 0, an early shutoff will happen. @@ -63,9 +58,13 @@ impl Self { writable: vec![T; Self::CAPACITY_WORDS], data: vec![u8::MAX; Self::CAPACITY_BYTES], - erases: 0, - reads: 0, - writes: 0, + current_stats: FlashStatsSnapshot { + erases: 0, + reads: 0, + writes: 0, + bytes_read: 0, + bytes_written: 0, + }, write_count_check, bytes_until_shutoff, alignment_check, @@ -109,6 +108,11 @@ impl } } + /// Get a snapshot of the performance counters + pub fn stats_snapshot(&self) -> FlashStatsSnapshot { + self.current_stats + } + #[cfg(feature = "_test")] /// Print all items in flash to the returned string pub fn print_items(&mut self) -> String { @@ -232,7 +236,8 @@ impl R const READ_SIZE: usize = BYTES_PER_WORD; async fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), Self::Error> { - self.reads += 1; + self.current_stats.reads += 1; + self.current_stats.bytes_read += bytes.len() as u64; if bytes.len() % Self::READ_SIZE != 0 { panic!("any read must be a multiple of Self::READ_SIZE bytes"); @@ -263,7 +268,7 @@ impl N const ERASE_SIZE: usize = Self::PAGE_BYTES; async fn erase(&mut self, from: u32, to: u32) -> Result<(), Self::Error> { - self.erases += 1; + self.current_stats.erases += 1; let from = from as usize; let to = to as usize; @@ -291,7 +296,7 @@ impl N } async fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Self::Error> { - self.writes += 1; + self.current_stats.writes += 1; let range = Self::validate_operation(offset, bytes.len())?; @@ -333,6 +338,8 @@ impl N }; } + self.current_stats.bytes_written += 1; + self.as_bytes_mut()[address + byte_index] &= byte; } } @@ -379,3 +386,154 @@ pub enum WriteCountCheck { /// No check at all Disabled, } + +/// A snapshot of the flash performance statistics +#[derive(Debug, Clone, Copy)] +pub struct FlashStatsSnapshot { + erases: u64, + reads: u64, + writes: u64, + bytes_read: u64, + bytes_written: u64, +} + +impl FlashStatsSnapshot { + /// Compare the snapshot to another snapshot. + /// + /// The oldest snapshot goes first, so it's `old.compare_to(new)`. + pub fn compare_to(&self, other: Self) -> FlashStatsResult { + FlashStatsResult { + erases: other + .erases + .checked_sub(self.erases) + .expect("Order is old compare to new"), + reads: other + .reads + .checked_sub(self.reads) + .expect("Order is old compare to new"), + writes: other + .writes + .checked_sub(self.writes) + .expect("Order is old compare to new"), + bytes_read: other + .bytes_read + .checked_sub(self.bytes_read) + .expect("Order is old compare to new"), + bytes_written: other + .bytes_written + .checked_sub(self.bytes_written) + .expect("Order is old compare to new"), + } + } +} + +/// The performance stats of everything between two snapshots +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct FlashStatsResult { + /// The amount of times a page has been erased + pub erases: u64, + /// The amount of times a read operation was started + pub reads: u64, + /// The amount of times a write operation was started + pub writes: u64, + /// The total amount of bytes that were read + pub bytes_read: u64, + /// The total amount of bytes that were written + pub bytes_written: u64, +} + +impl FlashStatsResult { + /// Take the average of the stats + pub fn take_average(&self, divider: u64) -> FlashAverageStatsResult { + FlashAverageStatsResult { + avg_erases: self.erases as f64 / divider as f64, + avg_reads: self.reads as f64 / divider as f64, + avg_writes: self.writes as f64 / divider as f64, + avg_bytes_read: self.bytes_read as f64 / divider as f64, + avg_bytes_written: self.bytes_written as f64 / divider as f64, + } + } +} + +impl AddAssign for FlashStatsResult { + fn add_assign(&mut self, rhs: Self) { + *self = *self + rhs; + } +} + +impl Add for FlashStatsResult { + type Output = FlashStatsResult; + + fn add(self, rhs: Self) -> Self::Output { + Self { + erases: self.erases + rhs.erases, + reads: self.reads + rhs.reads, + writes: self.writes + rhs.writes, + bytes_read: self.bytes_read + rhs.bytes_read, + bytes_written: self.bytes_written + rhs.bytes_written, + } + } +} + +/// The averaged performance stats of everything between two snapshots +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub struct FlashAverageStatsResult { + /// The amount of times a page has been erased + pub avg_erases: f64, + /// The amount of times a read operation was started + pub avg_reads: f64, + /// The amount of times a write operation was started + pub avg_writes: f64, + /// The total amount of bytes that were read + pub avg_bytes_read: f64, + /// The total amount of bytes that were written + pub avg_bytes_written: f64, +} + +impl approx::AbsDiffEq for FlashAverageStatsResult { + type Epsilon = f64; + + fn default_epsilon() -> Self::Epsilon { + f64::EPSILON + } + + fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool { + self.avg_erases.abs_diff_eq(&other.avg_erases, epsilon) + && self.avg_reads.abs_diff_eq(&other.avg_reads, epsilon) + && self.avg_writes.abs_diff_eq(&other.avg_writes, epsilon) + && self + .avg_bytes_read + .abs_diff_eq(&other.avg_bytes_read, epsilon) + && self + .avg_bytes_written + .abs_diff_eq(&other.avg_bytes_written, epsilon) + } +} + +impl approx::RelativeEq for FlashAverageStatsResult { + fn default_max_relative() -> Self::Epsilon { + f64::default_max_relative() + } + + fn relative_eq( + &self, + other: &Self, + epsilon: Self::Epsilon, + max_relative: Self::Epsilon, + ) -> bool { + self.avg_erases + .relative_eq(&other.avg_erases, epsilon, max_relative) + && self + .avg_reads + .relative_eq(&other.avg_reads, epsilon, max_relative) + && self + .avg_writes + .relative_eq(&other.avg_writes, epsilon, max_relative) + && self + .avg_bytes_read + .relative_eq(&other.avg_bytes_read, epsilon, max_relative) + && self + .avg_bytes_written + .relative_eq(&other.avg_bytes_written, epsilon, max_relative) + } +} diff --git a/src/queue.rs b/src/queue.rs index cc00177..7f87294 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -605,7 +605,7 @@ pub async fn try_repair( #[cfg(test)] mod tests { use crate::cache::PrivateCacheImpl; - use crate::mock_flash::WriteCountCheck; + use crate::mock_flash::{FlashAverageStatsResult, FlashStatsResult, WriteCountCheck}; use super::*; use futures_test::test; @@ -905,9 +905,12 @@ mod tests { let flash_range = 0x000..0x1000; let mut data_buffer = AlignedBuf([0; 1024]); - let mut push_ops = (0, 0, 0, 0); - let mut peek_ops = (0, 0, 0, 0); - let mut pop_ops = (0, 0, 0, 0); + let mut push_stats = FlashStatsResult::default(); + let mut pushes = 0; + let mut peek_stats = FlashStatsResult::default(); + let mut peeks = 0; + let mut pop_stats = FlashStatsResult::default(); + let mut pops = 0; let mut cache = cache::NoCache::new(); @@ -915,82 +918,125 @@ mod tests { println!("Loop index: {loop_index}"); for i in 0..20 { + let start_snapshot = flash.stats_snapshot(); let data = vec![i as u8; 50]; push(&mut flash, flash_range.clone(), &mut cache, &data, false) .await .unwrap(); - add_ops(&mut flash, &mut push_ops); + pushes += 1; + push_stats += start_snapshot.compare_to(flash.stats_snapshot()); } + let start_snapshot = flash.stats_snapshot(); let mut peeker = peek_many(&mut flash, flash_range.clone(), &mut cache) .await .unwrap(); + peek_stats += start_snapshot.compare_to(peeker.iter.flash.stats_snapshot()); for i in 0..5 { + let start_snapshot = peeker.iter.flash.stats_snapshot(); let mut data = vec![i as u8; 50]; assert_eq!( peeker.next(&mut data_buffer).await.unwrap(), Some(&mut data[..]), "At {i}" ); - add_ops(peeker.iter.flash, &mut peek_ops); + peeks += 1; + peek_stats += start_snapshot.compare_to(peeker.iter.flash.stats_snapshot()); } + let start_snapshot = flash.stats_snapshot(); let mut popper = pop_many(&mut flash, flash_range.clone(), &mut cache) .await .unwrap(); + pop_stats += start_snapshot.compare_to(popper.iter.flash.stats_snapshot()); for i in 0..5 { + let start_snapshot = popper.iter.flash.stats_snapshot(); let data = vec![i as u8; 50]; assert_eq!( &popper.next(&mut data_buffer).await.unwrap().unwrap()[..], &data, "At {i}" ); - add_ops(popper.iter.flash, &mut pop_ops); + pops += 1; + pop_stats += start_snapshot.compare_to(popper.iter.flash.stats_snapshot()); } for i in 20..25 { + let start_snapshot = flash.stats_snapshot(); let data = vec![i as u8; 50]; push(&mut flash, flash_range.clone(), &mut cache, &data, false) .await .unwrap(); - add_ops(&mut flash, &mut push_ops); + pushes += 1; + push_stats += start_snapshot.compare_to(flash.stats_snapshot()); } + let start_snapshot = flash.stats_snapshot(); let mut peeker = peek_many(&mut flash, flash_range.clone(), &mut cache) .await .unwrap(); + peek_stats += start_snapshot.compare_to(peeker.iter.flash.stats_snapshot()); for i in 5..25 { + let start_snapshot = peeker.iter.flash.stats_snapshot(); let data = vec![i as u8; 50]; assert_eq!( &peeker.next(&mut data_buffer).await.unwrap().unwrap()[..], &data, "At {i}" ); - add_ops(peeker.iter.flash, &mut peek_ops); + peeks += 1; + peek_stats += start_snapshot.compare_to(peeker.iter.flash.stats_snapshot()); } + let start_snapshot = flash.stats_snapshot(); let mut popper = pop_many(&mut flash, flash_range.clone(), &mut cache) .await .unwrap(); + pop_stats += start_snapshot.compare_to(popper.iter.flash.stats_snapshot()); for i in 5..25 { + let start_snapshot = popper.iter.flash.stats_snapshot(); let data = vec![i as u8; 50]; assert_eq!( &popper.next(&mut data_buffer).await.unwrap().unwrap()[..], &data, "At {i}" ); - add_ops(popper.iter.flash, &mut pop_ops); + pops += 1; + pop_stats += start_snapshot.compare_to(popper.iter.flash.stats_snapshot()); } } // Assert the performance. These numbers can be changed if acceptable. - // Format = (writes, reads, erases, num operations) - println!("Asserting push ops:"); - assert_avg_ops(&push_ops, (3.1252, 17.902, 0.0612)); - println!("Asserting peek ops:"); - assert_avg_ops(&peek_ops, (0.0, 8.0188, 0.0)); - println!("Asserting pop ops:"); - assert_avg_ops(&pop_ops, (1.0, 8.0188, 0.0)); + approx::assert_relative_eq!( + push_stats.take_average(pushes), + FlashAverageStatsResult { + avg_erases: 0.0612, + avg_reads: 17.902, + avg_writes: 3.1252, + avg_bytes_read: 113.7248, + avg_bytes_written: 60.5008 + } + ); + approx::assert_relative_eq!( + peek_stats.take_average(peeks), + FlashAverageStatsResult { + avg_erases: 0.0, + avg_reads: 8.0188, + avg_writes: 0.0, + avg_bytes_read: 96.4224, + avg_bytes_written: 0.0 + } + ); + approx::assert_relative_eq!( + pop_stats.take_average(pops), + FlashAverageStatsResult { + avg_erases: 0.0, + avg_reads: 8.0188, + avg_writes: 1.0, + avg_bytes_read: 96.4224, + avg_bytes_written: 8.0 + } + ); } #[test] @@ -999,13 +1045,16 @@ mod tests { let flash_range = 0x000..0x1000; let mut data_buffer = AlignedBuf([0; 1024]); - let mut push_ops = (0, 0, 0, 0); - let mut pop_ops = (0, 0, 0, 0); + let mut push_stats = FlashStatsResult::default(); + let mut pushes = 0; + let mut pop_stats = FlashStatsResult::default(); + let mut pops = 0; for loop_index in 0..100 { println!("Loop index: {loop_index}"); for i in 0..20 { + let start_snapshot = flash.stats_snapshot(); let data = vec![i as u8; 50]; push( &mut flash, @@ -1016,10 +1065,12 @@ mod tests { ) .await .unwrap(); - add_ops(&mut flash, &mut push_ops); + pushes += 1; + push_stats += start_snapshot.compare_to(flash.stats_snapshot()); } for i in 0..5 { + let start_snapshot = flash.stats_snapshot(); let data = vec![i as u8; 50]; assert_eq!( &pop( @@ -1034,10 +1085,12 @@ mod tests { &data, "At {i}" ); - add_ops(&mut flash, &mut pop_ops); + pops += 1; + pop_stats += start_snapshot.compare_to(flash.stats_snapshot()); } for i in 20..25 { + let start_snapshot = flash.stats_snapshot(); let data = vec![i as u8; 50]; push( &mut flash, @@ -1048,10 +1101,12 @@ mod tests { ) .await .unwrap(); - add_ops(&mut flash, &mut push_ops); + pushes += 1; + push_stats += start_snapshot.compare_to(flash.stats_snapshot()); } for i in 5..25 { + let start_snapshot = flash.stats_snapshot(); let data = vec![i as u8; 50]; assert_eq!( &pop( @@ -1066,48 +1121,32 @@ mod tests { &data, "At {i}" ); - add_ops(&mut flash, &mut pop_ops); + pops += 1; + pop_stats += start_snapshot.compare_to(flash.stats_snapshot()); } } // Assert the performance. These numbers can be changed if acceptable. - // Format = (writes, reads, erases, num operations) - println!("Asserting push ops:"); - assert_avg_ops(&push_ops, (3.1252, 17.902, 0.0612)); - println!("Asserting pop ops:"); - assert_avg_ops(&pop_ops, (1.0, 82.618, 0.0)); - } - - fn add_ops(flash: &mut MockFlashBig, ops: &mut (u32, u32, u32, u32)) { - ops.0 += core::mem::replace(&mut flash.writes, 0); - ops.1 += core::mem::replace(&mut flash.reads, 0); - ops.2 += core::mem::replace(&mut flash.erases, 0); - ops.3 += 1; - } - - #[track_caller] - fn assert_avg_ops(ops: &(u32, u32, u32, u32), expected_averages: (f32, f32, f32)) { - let averages = ( - ops.0 as f32 / ops.3 as f32, - ops.1 as f32 / ops.3 as f32, - ops.2 as f32 / ops.3 as f32, - ); - - println!( - "Average writes: {}, expected: {}", - averages.0, expected_averages.0 - ); - println!( - "Average reads: {}, expected: {}", - averages.1, expected_averages.1 + approx::assert_relative_eq!( + push_stats.take_average(pushes), + FlashAverageStatsResult { + avg_erases: 0.0612, + avg_reads: 17.902, + avg_writes: 3.1252, + avg_bytes_read: 113.7248, + avg_bytes_written: 60.5008 + } ); - println!( - "Average erases: {}, expected: {}", - averages.2, expected_averages.2 + approx::assert_relative_eq!( + pop_stats.take_average(pops), + FlashAverageStatsResult { + avg_erases: 0.0, + avg_reads: 82.618, + avg_writes: 1.0, + avg_bytes_read: 567.9904, + avg_bytes_written: 8.0 + } ); - approx::assert_relative_eq!(averages.0, expected_averages.0); - approx::assert_relative_eq!(averages.1, expected_averages.1); - approx::assert_relative_eq!(averages.2, expected_averages.2); } #[test]