From d97c7b9af604e103117a7adcee703bb6964667a9 Mon Sep 17 00:00:00 2001 From: funbiscuit Date: Sun, 21 Jan 2024 21:54:07 +0300 Subject: [PATCH] perf: use memory efficient search of str prefix --- README.md | 2 +- embedded-cli/src/autocomplete.rs | 13 +++------- embedded-cli/src/utils.rs | 42 ++++++++++++++++++++++++++++++++ examples/arduino/README.md | 16 ++++++------ 4 files changed, 55 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 4fe9b90..5871147 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ [Demo](examples/arduino/README.md) of CLI running on Arduino Nano. -Memory usage: 15KiB of ROM and 0.5KiB of static RAM. Most of static RAM is used by help strings. +Memory usage: 14KiB of ROM and 0.5KiB of static RAM. Most of static RAM is used by help strings. ![Arduino Demo](examples/arduino/demo.gif) diff --git a/embedded-cli/src/autocomplete.rs b/embedded-cli/src/autocomplete.rs index ad0c2f3..9f65b2a 100644 --- a/embedded-cli/src/autocomplete.rs +++ b/embedded-cli/src/autocomplete.rs @@ -48,7 +48,7 @@ impl<'a> Autocompletion<'a> { } /// Whether autocompletion is partial - /// and futher input is required + /// and further input is required pub fn is_partial(&self) -> bool { self.partial } @@ -71,17 +71,12 @@ impl<'a> Autocompletion<'a> { // compare new autocompletion to existing and keep // only common prefix let len = match self.autocompleted() { - Some(current) => autocompletion - .char_indices() - .zip(current.char_indices()) - .find(|((_, a1), (_, a2))| a1 != a2) - .map(|((pos, _), _)| pos) - .unwrap_or(current.len().min(autocompletion.len())), + Some(current) => utils::common_prefix_len(autocompletion, current), None => autocompletion.len(), }; if len > self.buffer.len() { - // if buffer is full with this autocompletion, there is not much sence in doing it + // if buffer is full with this autocompletion, there is not much sense in doing it // since user will not be able to type anything else // so just do nothing with it } else { @@ -100,7 +95,7 @@ mod tests { use crate::autocomplete::Autocompletion; #[test] - fn nomerge() { + fn no_merge() { let mut input = [0; 64]; let autocompletion = Autocompletion::new(&mut input); diff --git a/embedded-cli/src/utils.rs b/embedded-cli/src/utils.rs index 709589f..b51ab96 100644 --- a/embedded-cli/src/utils.rs +++ b/embedded-cli/src/utils.rs @@ -34,6 +34,27 @@ pub fn char_count(text: &str) -> usize { count } +/// Returns length (in bytes) of longest common prefix +pub fn common_prefix_len(left: &str, right: &str) -> usize { + let mut accum1 = Utf8Accum::default(); + + let mut pos = 0; + let mut byte_counter = 0; + + for (&b1, &b2) in left.as_bytes().iter().zip(right.as_bytes().iter()) { + if b1 != b2 { + break; + } + let c1 = accum1.push_byte(b1); + byte_counter += 1; + if c1.is_some() { + pos = byte_counter; + } + } + + pos +} + /// Function to rotate `buf` by `mid` elements /// /// Not using `core::slice::rotate_left` since it @@ -130,4 +151,25 @@ mod tests { fn char_count(#[case] text: &str) { assert_eq!(utils::char_count(text), text.chars().count()) } + + #[rstest] + #[case("abcdef", "abcdef")] + #[case("abcdef", "abc")] + #[case("abcdef", "abc ghf")] + #[case("abcdef", "")] + #[case("", "")] + #[case("абв 佐佗佟𑿁", "абв 佐佗佟𑿁")] + #[case("абв 佐佗佟𑿁𑿆𑿌", "абв 佐佗佟𑿁")] + #[case("абв 佐佗佟𑿁 𑿆𑿌", "абв 佐佗𑿁佟")] + fn common_prefix(#[case] left: &str, #[case] right: &str) { + let expected = left + .char_indices() + .zip(right.char_indices()) + .find(|((_, a1), (_, a2))| a1 != a2) + .map(|((pos, _), _)| pos) + .unwrap_or(right.len().min(left.len())); + + assert_eq!(utils::common_prefix_len(left, right), expected); + assert_eq!(utils::common_prefix_len(right, left), expected); + } } diff --git a/examples/arduino/README.md b/examples/arduino/README.md index b1a89e8..79b63c6 100644 --- a/examples/arduino/README.md +++ b/examples/arduino/README.md @@ -2,7 +2,7 @@ This example shows how to build cli with Arduino Nano. Another Arduino can also be used, but you will have to tweak configs. -Example uses ~15KiB of ROM and ~0.5KiB of static RAM. +Example uses ~14KiB of ROM and ~0.5KiB of static RAM. Most of RAM is taken by derived implementations for help and autocomplete that don't use progmem. In future this should be fixed. @@ -33,13 +33,13 @@ cargo bloat --release Example output: ``` File .text Size Crate Name -1.6% 54.5% 8.1KiB arduino_cli arduino_cli::try_run -0.2% 6.7% 1016B embedded_cli embedded_cli::autocomplete::Autocompletion::merge_autocompletion -0.1% 3.6% 538B embedded_cli embedded_cli::token::Tokens::new -0.1% 3.5% 530B embedded_cli embedded_cli::help::HelpRequest::from_tokens -0.1% 3.1% 472B embedded_cli embedded_cli::token::Tokens::remove -0.8% 25.4% 3.8KiB And 34 smaller methods. Use -n N to show more. -3.0% 100.0% 14.8KiB .text section size, the file size is 498.4KiB +1.6% 56.9% 8.1KiB arduino_cli arduino_cli::try_run +0.1% 3.7% 538B embedded_cli embedded_cli::token::Tokens::new +0.1% 3.6% 530B embedded_cli embedded_cli::help::HelpRequest::from_tokens +0.1% 3.2% 472B embedded_cli embedded_cli::token::Tokens::remove +0.1% 3.2% 468B embedded_cli embedded_cli::cli::Cli::navigate_history +0.7% 26.0% 3.7KiB And 34 smaller methods. Use -n N to show more. +2.9% 100.0% 14.2KiB .text section size, the file size is 493.3KiB ``` To find total static memory usage: