From 325ec690249438ead86fb2a01253b4761527ccae Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Mon, 17 Nov 2025 17:40:59 +0300 Subject: [PATCH 1/4] chore: tag 0.4.10 Signed-off-by: NotAShelf Change-Id: I6a2158a305d5f249b52c8b21dc5aaca86a6a6964 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 40f74fc..c7bf6b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -772,7 +772,7 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "microfetch" -version = "0.4.9" +version = "0.4.10" dependencies = [ "criterion", "hotpath", diff --git a/Cargo.toml b/Cargo.toml index 6af4917..c97b58b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "microfetch" -version = "0.4.9" +version = "0.4.10" edition = "2024" [lib] From 2ad765ef988051fd7fb6272c595baee1ed87a00c Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Mon, 17 Nov 2025 17:55:10 +0300 Subject: [PATCH 2/4] various: reduce allocations where available Signed-off-by: NotAShelf Change-Id: I517d855b14c015569a325deb64948f3b6a6a6964 --- src/colors.rs | 45 +++++++++++++++++++++++++++--------- src/desktop.rs | 47 ++++++++++++++++++++++---------------- src/release.rs | 28 ++++++++++++++--------- src/system.rs | 62 ++++++++++++++++++++++++++++++++++---------------- src/uptime.rs | 8 +++---- 5 files changed, 125 insertions(+), 65 deletions(-) diff --git a/src/colors.rs b/src/colors.rs index 53b12f6..07ab9bd 100644 --- a/src/colors.rs +++ b/src/colors.rs @@ -37,7 +37,7 @@ impl Colors { } pub static COLORS: LazyLock = LazyLock::new(|| { - // check for NO_COLOR once at startup + // Check for NO_COLOR once at startup let is_no_color = env::var("NO_COLOR").is_ok(); Colors::new(is_no_color) }); @@ -45,14 +45,37 @@ pub static COLORS: LazyLock = LazyLock::new(|| { #[must_use] #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn print_dots() -> String { - format!( - "{} {} {} {} {} {} {}", - COLORS.blue, - COLORS.cyan, - COLORS.green, - COLORS.yellow, - COLORS.red, - COLORS.magenta, - COLORS.reset, - ) + // Pre-calculate capacity: 6 color codes + " " (glyph + 2 spaces) per color + const GLYPH: &str = ""; + let capacity = COLORS.blue.len() + + COLORS.cyan.len() + + COLORS.green.len() + + COLORS.yellow.len() + + COLORS.red.len() + + COLORS.magenta.len() + + COLORS.reset.len() + + (GLYPH.len() + 2) * 6; + + let mut result = String::with_capacity(capacity); + result.push_str(COLORS.blue); + result.push_str(GLYPH); + result.push_str(" "); + result.push_str(COLORS.cyan); + result.push_str(GLYPH); + result.push_str(" "); + result.push_str(COLORS.green); + result.push_str(GLYPH); + result.push_str(" "); + result.push_str(COLORS.yellow); + result.push_str(GLYPH); + result.push_str(" "); + result.push_str(COLORS.red); + result.push_str(GLYPH); + result.push_str(" "); + result.push_str(COLORS.magenta); + result.push_str(GLYPH); + result.push_str(" "); + result.push_str(COLORS.reset); + + result } diff --git a/src/desktop.rs b/src/desktop.rs index 7d3733a..561be03 100644 --- a/src/desktop.rs +++ b/src/desktop.rs @@ -1,30 +1,37 @@ +use std::fmt::Write; + #[must_use] #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn get_desktop_info() -> String { // Retrieve the environment variables and handle Result types let desktop_env = std::env::var("XDG_CURRENT_DESKTOP"); - let display_backend_result = std::env::var("XDG_SESSION_TYPE"); + let display_backend = std::env::var("XDG_SESSION_TYPE"); - // Capitalize the first letter of the display backend value - let mut display_backend = display_backend_result.unwrap_or_default(); - if let Some(c) = display_backend.as_mut_str().get_mut(0..1) { - c.make_ascii_uppercase(); - } - - // Trim "none+" from the start of desktop_env if present - // Use "Unknown" if desktop_env is empty or has an error - let desktop_env = match desktop_env { - Err(_) => "Unknown".to_owned(), - Ok(s) => s.trim_start_matches("none+").to_owned(), + let desktop_str = match desktop_env { + Err(_) => "Unknown", + Ok(ref s) if s.starts_with("none+") => &s[5..], + Ok(ref s) => s.as_str(), }; - // Handle the case where display_backend might be empty after capitalization - let display_backend = if display_backend.is_empty() { - "Unknown" - } else { - &display_backend - } - .to_owned(); + let backend_str = match display_backend { + Err(_) => "Unknown", + Ok(ref s) if s.is_empty() => "Unknown", + Ok(ref s) => s.as_str(), + }; - format!("{desktop_env} ({display_backend})") + // Pre-calculate capacity: desktop_len + " (" + backend_len + ")" + // Capitalize first char needs temporary allocation only if backend exists + let mut result = + String::with_capacity(desktop_str.len() + backend_str.len() + 3); + result.push_str(desktop_str); + result.push_str(" ("); + + // Capitalize first character of backend + if let Some(first_char) = backend_str.chars().next() { + let _ = write!(result, "{}", first_char.to_ascii_uppercase()); + result.push_str(&backend_str[first_char.len_utf8()..]); + } + + result.push(')'); + result } diff --git a/src/release.rs b/src/release.rs index 2f1338c..d9ec4c9 100644 --- a/src/release.rs +++ b/src/release.rs @@ -1,6 +1,7 @@ use std::{ + fmt::Write as _, fs::File, - io::{self, BufRead, BufReader}, + io::{self, Read}, }; use nix::sys::utsname::UtsName; @@ -8,21 +9,26 @@ use nix::sys::utsname::UtsName; #[must_use] #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn get_system_info(utsname: &UtsName) -> String { - format!( - "{} {} ({})", - utsname.sysname().to_str().unwrap_or("Unknown"), - utsname.release().to_str().unwrap_or("Unknown"), - utsname.machine().to_str().unwrap_or("Unknown") - ) + let sysname = utsname.sysname().to_str().unwrap_or("Unknown"); + let release = utsname.release().to_str().unwrap_or("Unknown"); + let machine = utsname.machine().to_str().unwrap_or("Unknown"); + + // Pre-allocate capacity: sysname + " " + release + " (" + machine + ")" + let capacity = sysname.len() + 1 + release.len() + 2 + machine.len() + 1; + let mut result = String::with_capacity(capacity); + + write!(result, "{sysname} {release} ({machine})").unwrap(); + result } #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn get_os_pretty_name() -> Result { - let file = File::open("/etc/os-release")?; - let reader = BufReader::new(file); + // We use a stack-allocated buffer here, which seems to perform MUCH better + // than `BufReader`. In hindsight, I should've seen this coming. + let mut buffer = String::with_capacity(1024); + File::open("/etc/os-release")?.read_to_string(&mut buffer)?; - for line in reader.lines() { - let line = line?; + for line in buffer.lines() { if let Some(pretty_name) = line.strip_prefix("PRETTY_NAME=") { if let Some(trimmed) = pretty_name .strip_prefix('"') diff --git a/src/system.rs b/src/system.rs index f90c912..21b2b2f 100644 --- a/src/system.rs +++ b/src/system.rs @@ -1,5 +1,6 @@ use std::{ env, + fmt::Write as _, fs::File, io::{self, Read}, }; @@ -12,18 +13,26 @@ use crate::colors::COLORS; #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn get_username_and_hostname(utsname: &UtsName) -> String { let username = env::var("USER").unwrap_or_else(|_| "unknown_user".to_owned()); - let hostname = utsname - .nodename() - .to_str() - .unwrap_or("unknown_host") - .to_owned(); - format!( - "{yellow}{username}{red}@{green}{hostname}{reset}", - yellow = COLORS.yellow, - red = COLORS.red, - green = COLORS.green, - reset = COLORS.reset, - ) + let hostname = utsname.nodename().to_str().unwrap_or("unknown_host"); + + let capacity = COLORS.yellow.len() + + username.len() + + COLORS.red.len() + + 1 + + COLORS.green.len() + + hostname.len() + + COLORS.reset.len(); + let mut result = String::with_capacity(capacity); + + result.push_str(COLORS.yellow); + result.push_str(&username); + result.push_str(COLORS.red); + result.push('@'); + result.push_str(COLORS.green); + result.push_str(hostname); + result.push_str(COLORS.reset); + + result } #[must_use] @@ -31,8 +40,13 @@ pub fn get_username_and_hostname(utsname: &UtsName) -> String { pub fn get_shell() -> String { let shell_path = env::var("SHELL").unwrap_or_else(|_| "unknown_shell".to_owned()); - let shell_name = shell_path.rsplit('/').next().unwrap_or("unknown_shell"); - shell_name.to_owned() + + // Find last '/' and get the part after it, avoiding allocation + shell_path + .rsplit('/') + .next() + .unwrap_or("unknown_shell") + .to_owned() } #[cfg_attr(feature = "hotpath", hotpath::measure)] @@ -49,11 +63,16 @@ pub fn get_root_disk_usage() -> Result { let used_size = used_size as f64 / (1024.0 * 1024.0 * 1024.0); let usage = (used_size / total_size) * 100.0; - Ok(format!( + let mut result = String::with_capacity(64); + write!( + result, "{used_size:.2} GiB / {total_size:.2} GiB ({cyan}{usage:.0}%{reset})", cyan = COLORS.cyan, reset = COLORS.reset, - )) + ) + .unwrap(); + + Ok(result) } #[cfg_attr(feature = "hotpath", hotpath::measure)] @@ -70,7 +89,7 @@ pub fn get_memory_usage() -> Result { let mut split = line.split_whitespace(); match split.next().unwrap_or_default() { "MemTotal:" => { - total_memory_kb = split.next().unwrap_or("0").parse().unwrap_or(0.0) + total_memory_kb = split.next().unwrap_or("0").parse().unwrap_or(0.0); }, "MemAvailable:" => { available_memory_kb = @@ -92,10 +111,15 @@ pub fn get_memory_usage() -> Result { let (used_memory, total_memory) = parse_memory_info()?; let percentage_used = (used_memory / total_memory * 100.0).round() as u64; - Ok(format!( + let mut result = String::with_capacity(64); + write!( + result, "{used_memory:.2} GiB / {total_memory:.2} GiB \ ({cyan}{percentage_used}%{reset})", cyan = COLORS.cyan, reset = COLORS.reset, - )) + ) + .unwrap(); + + Ok(result) } diff --git a/src/uptime.rs b/src/uptime.rs index d5253d6..bd2cc39 100644 --- a/src/uptime.rs +++ b/src/uptime.rs @@ -1,4 +1,4 @@ -use std::{io, mem::MaybeUninit}; +use std::{fmt::Write, io, mem::MaybeUninit}; #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn get_current() -> Result { @@ -16,21 +16,21 @@ pub fn get_current() -> Result { let mut result = String::with_capacity(32); if days > 0 { - result.push_str(&days.to_string()); + let _ = write!(result, "{days}"); result.push_str(if days == 1 { " day" } else { " days" }); } if hours > 0 { if !result.is_empty() { result.push_str(", "); } - result.push_str(&hours.to_string()); + let _ = write!(result, "{hours}"); result.push_str(if hours == 1 { " hour" } else { " hours" }); } if minutes > 0 { if !result.is_empty() { result.push_str(", "); } - result.push_str(&minutes.to_string()); + let _ = write!(result, "{minutes}"); result.push_str(if minutes == 1 { " minute" } else { " minutes" }); } if result.is_empty() { From f4f3385ff760f6570ae7af93774748e977da30e4 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Mon, 17 Nov 2025 18:17:59 +0300 Subject: [PATCH 3/4] various: fix clippy warnings - Adds proper documentation comments with `# Errors` sections for all functions returning `Result` - `cast_precision_loss` on `u64` -> `f64` for disk sizes is acceptable since disk sizes won't exceed f64's precision limit in practice. Thus, we can suppress those. - `cast_sign_loss` and `cast_possible_truncation` on the percentage calculation is safe since percentages are always 0-100. Once again, it's safe to suppress. Signed-off-by: NotAShelf Change-Id: Id4dd7ebc9674407d2be4f38ff4de24bc6a6a6964 --- src/release.rs | 5 +++++ src/system.rs | 12 ++++++++++++ src/uptime.rs | 6 ++++++ 3 files changed, 23 insertions(+) diff --git a/src/release.rs b/src/release.rs index d9ec4c9..1b820b8 100644 --- a/src/release.rs +++ b/src/release.rs @@ -21,6 +21,11 @@ pub fn get_system_info(utsname: &UtsName) -> String { result } +/// Gets the pretty name of the OS from `/etc/os-release`. +/// +/// # Errors +/// +/// Returns an error if `/etc/os-release` cannot be read. #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn get_os_pretty_name() -> Result { // We use a stack-allocated buffer here, which seems to perform MUCH better diff --git a/src/system.rs b/src/system.rs index 21b2b2f..ba90b27 100644 --- a/src/system.rs +++ b/src/system.rs @@ -49,7 +49,13 @@ pub fn get_shell() -> String { .to_owned() } +/// Gets the root disk usage information. +/// +/// # Errors +/// +/// Returns an error if the filesystem information cannot be retrieved. #[cfg_attr(feature = "hotpath", hotpath::measure)] +#[allow(clippy::cast_precision_loss)] pub fn get_root_disk_usage() -> Result { let vfs = statvfs("/")?; let block_size = vfs.block_size() as u64; @@ -75,6 +81,11 @@ pub fn get_root_disk_usage() -> Result { Ok(result) } +/// Gets the system memory usage information. +/// +/// # Errors +/// +/// Returns an error if `/proc/meminfo` cannot be read. #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn get_memory_usage() -> Result { #[cfg_attr(feature = "hotpath", hotpath::measure)] @@ -109,6 +120,7 @@ pub fn get_memory_usage() -> Result { } let (used_memory, total_memory) = parse_memory_info()?; + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] let percentage_used = (used_memory / total_memory * 100.0).round() as u64; let mut result = String::with_capacity(64); diff --git a/src/uptime.rs b/src/uptime.rs index bd2cc39..72af399 100644 --- a/src/uptime.rs +++ b/src/uptime.rs @@ -1,5 +1,10 @@ use std::{fmt::Write, io, mem::MaybeUninit}; +/// Gets the current system uptime. +/// +/// # Errors +/// +/// Returns an error if the system uptime cannot be retrieved. #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn get_current() -> Result { let uptime_seconds = { @@ -7,6 +12,7 @@ pub fn get_current() -> Result { if unsafe { libc::sysinfo(info.as_mut_ptr()) } != 0 { return Err(io::Error::last_os_error()); } + #[allow(clippy::cast_sign_loss)] unsafe { info.assume_init().uptime as u64 } }; From 789ece866b0480f57a365f6c1509a51c8b90b005 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Mon, 17 Nov 2025 18:30:06 +0300 Subject: [PATCH 4/4] ci: initial benchmarking workflows Signed-off-by: NotAShelf Change-Id: I367444097eafbd1020c02707c42351bf6a6a6964 --- .github/workflows/hotpath-comment.yml | 57 ++++++++++++++++++++++++ .github/workflows/hotpath-profile.yml | 63 +++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 .github/workflows/hotpath-comment.yml create mode 100644 .github/workflows/hotpath-profile.yml diff --git a/.github/workflows/hotpath-comment.yml b/.github/workflows/hotpath-comment.yml new file mode 100644 index 0000000..395a533 --- /dev/null +++ b/.github/workflows/hotpath-comment.yml @@ -0,0 +1,57 @@ +name: Hotpath Comment + +on: + workflow_run: + workflows: ["Hotpath Profile"] + types: + - completed + +permissions: + pull-requests: write + +jobs: + comment: + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' }} + + steps: + - name: Download profiling results + uses: actions/download-artifact@v4 + with: + name: hotpath-results + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} + + - name: Read PR number + id: pr + run: echo "number=$(cat pr_number.txt)" >> $GITHUB_OUTPUT + + - name: Setup Rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + + - name: Install hotpath CLI + run: cargo install hotpath + + - name: Post timing comparison comment + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + hotpath profile-pr \ + --repo ${{ github.repository }} \ + --pr-number ${{ steps.pr.outputs.number }} \ + --head-json head-timing.json \ + --base-json base-timing.json \ + --mode timing \ + --title "⏱️ Hotpath Timing Profile" + + - name: Post allocation comparison comment + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + hotpath profile-pr \ + --repo ${{ github.repository }} \ + --pr-number ${{ steps.pr.outputs.number }} \ + --head-json head-alloc.json \ + --base-json base-alloc.json \ + --mode alloc \ + --title "📊 Hotpath Allocation Profile" diff --git a/.github/workflows/hotpath-profile.yml b/.github/workflows/hotpath-profile.yml new file mode 100644 index 0000000..b367ca2 --- /dev/null +++ b/.github/workflows/hotpath-profile.yml @@ -0,0 +1,63 @@ +name: Hotpath Profile + +on: + pull_request: + branches: [ "main" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + profile: + runs-on: ubuntu-latest + + steps: + - name: Checkout PR HEAD + uses: actions/checkout@v4 + + - name: Setup Rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + + - name: Run timing profiling on HEAD + env: + HOTPATH_JSON: "true" + run: | + cargo run --features='hotpath' 2>&1 | grep '^{"hotpath_profiling_mode"' > head-timing.json + + - name: Run allocation profiling on HEAD + env: + HOTPATH_JSON: "true" + run: | + cargo run --features='hotpath,hotpath-alloc-count-total' 2>&1 | grep '^{"hotpath_profiling_mode"' > head-alloc.json + + - name: Checkout base branch + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.base.sha }} + + - name: Run timing profiling on base + env: + HOTPATH_JSON: "true" + run: | + cargo run --features='hotpath' 2>&1 | grep '^{"hotpath_profiling_mode"' > base-timing.json + + - name: Run allocation profiling on base + env: + HOTPATH_JSON: "true" + run: | + cargo run --features='hotpath,hotpath-alloc-count-total' 2>&1 | grep '^{"hotpath_profiling_mode"' > base-alloc.json + + - name: Save PR number + run: echo "${{ github.event.number }}" > pr_number.txt + + - name: Upload profiling results + uses: actions/upload-artifact@v4 + with: + name: hotpath-results + path: | + head-timing.json + head-alloc.json + base-timing.json + base-alloc.json + pr_number.txt + retention-days: 1