From 8fd2aed6d730613ce1794bdd4e1ec89400be8893 Mon Sep 17 00:00:00 2001 From: Samsie Date: Sat, 4 Jul 2026 17:18:01 +0530 Subject: [PATCH 1/2] feat(cli): add --diff-only export mode to filter unchanged blocks This commit adds the --diff-only argument to the CI export capabilities, which filters the exported heap blocks to include only those that are new or changed relative to the baseline snapshot taken at startup. This ensures that massive JSON/CSV files don't become bogged down with identical unmodified blocks. --- src/ci.rs | 20 +++++++++++++++++++- src/types.rs | 2 +- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/ci.rs b/src/ci.rs index a9fcd4f..daad81e 100644 --- a/src/ci.rs +++ b/src/ci.rs @@ -19,6 +19,7 @@ struct CiArgs { duration: Option, format: Option, output: Option, + diff_only: bool, } pub fn ci_main(args: &[String]) -> i32 { @@ -47,6 +48,11 @@ pub fn ci_main(args: &[String]) -> i32 { None }; + if parsed.diff_only && !parsed.leak_check { + eprintln!("error: --diff-only requires --leak-check to be active"); + return 1; + } + let start = Instant::now(); let poll_interval = Duration::from_millis(1000); let mut sys = System::new_all(); @@ -113,7 +119,13 @@ pub fn ci_main(args: &[String]) -> i32 { if let Some(ref format_type) = parsed.format { if let Some(current_heap) = last_captured_heap { - let blocks = current_heap; + let mut blocks = current_heap; + + if parsed.diff_only { + if let Some(ref prev) = baseline { + blocks = blocks.into_iter().filter(|b| !prev.contains(b)).collect(); + } + } let target_path = parsed.output.clone().unwrap_or_else(|| match format_type { FormatType::Json => "heap_dump.json".to_string(), @@ -176,6 +188,7 @@ fn resolve_target(target: &CiTarget) -> Result<(u32, Option), AppError> { fn parse_ci_args(args: &[String]) -> Result { let mut max_memory = None; let mut leak_check = false; + let mut diff_only = false; let mut duration = None; let mut target = None; let mut format = None; @@ -199,6 +212,10 @@ fn parse_ci_args(args: &[String]) -> Result { leak_check = true; i += 1; } + "--diff-only" => { + diff_only = true; + i += 1; + } "--duration" => { if i + 1 < args.len() { let val = args[i + 1] @@ -289,6 +306,7 @@ fn parse_ci_args(args: &[String]) -> Result { target, max_memory, leak_check, + diff_only, duration, format, output, diff --git a/src/types.rs b/src/types.rs index 621c6fd..bf3fc44 100644 --- a/src/types.rs +++ b/src/types.rs @@ -50,7 +50,7 @@ pub enum RegionProtect { Other, } -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug, PartialEq, Serialize)] pub struct HeapBlock { pub address: usize, pub size: usize, From 73d4eb857a4616ed72adc54b113abb6dedbaae32 Mon Sep 17 00:00:00 2001 From: Samsie Date: Sat, 4 Jul 2026 17:46:26 +0530 Subject: [PATCH 2/2] style: fix formatting issues --- src/os/windows.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/os/windows.rs b/src/os/windows.rs index c5d0d85..cfc4543 100644 --- a/src/os/windows.rs +++ b/src/os/windows.rs @@ -139,6 +139,7 @@ pub fn walk_regions(pid: u32) -> Vec { /// A `Vec` with address, size, free status, and page protection /// for each parsed heap block. pub fn walk_heap(pid: u32) -> Vec { + use std::time::{Duration, Instant}; use windows::Win32::Foundation::CloseHandle; use windows::Win32::System::Diagnostics::Debug::ReadProcessMemory; use windows::Win32::System::Diagnostics::ToolHelp::{ @@ -150,7 +151,6 @@ pub fn walk_heap(pid: u32) -> Vec { use windows::Win32::System::Threading::{ OpenProcess, PROCESS_QUERY_INFORMATION, PROCESS_VM_READ, }; - use std::time::{Duration, Instant}; // --- tunable safety limits --- const MAX_BLOCKS_PER_HEAP: usize = 100_000; @@ -218,7 +218,9 @@ pub fn walk_heap(pid: u32) -> Vec { } if blocks_this_heap >= MAX_BLOCKS_PER_HEAP { // likely corrupted/hostile heap metadata — stop this heap, move to next - eprintln!("walk_heap: heap at {heap_base:#x} exceeded block cap, skipping rest"); + eprintln!( + "walk_heap: heap at {heap_base:#x} exceeded block cap, skipping rest" + ); break; }