From a53a04bda9d53a5528d41bef2a24a378eeda797f Mon Sep 17 00:00:00 2001 From: pmqueiroz Date: Sat, 25 Apr 2026 21:09:44 -0300 Subject: [PATCH 1/4] perf: replace fixed server sleep with readiness probe Polls localhost:1337 every 20ms instead of always waiting 100ms. Fast machines proceed as soon as the server is up. Co-Authored-By: Claude Sonnet 4.6 --- src/main.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 516fea6..f82c958 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,7 +28,15 @@ async fn main() -> Result<(), Box> { error!("❌ Server error: {}", e); } }); - tokio::time::sleep(Duration::from_millis(100)).await; + + // Probe server readiness instead of fixed sleep + let probe_url = format!("http://localhost:{}/", port); + for _ in 0..50 { + if reqwest::get(&probe_url).await.is_ok() { + break; + } + tokio::time::sleep(Duration::from_millis(20)).await; + } let provider: Box = match config.provider { ProviderType::Storybook => Box::new(StorybookProvider::new()), From 0beb2556b26e1ff9ab757ba125ef87091512aa32 Mon Sep 17 00:00:00 2001 From: pmqueiroz Date: Sat, 25 Apr 2026 21:10:36 -0300 Subject: [PATCH 2/4] fix: handle --update flag in diff step MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously --update had no effect on the diff phase — it still ran full comparisons. Now copies all snapshots to baselines in parallel and returns early, skipping the diff entirely. Co-Authored-By: Claude Sonnet 4.6 --- src/core/diff.rs | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/src/core/diff.rs b/src/core/diff.rs index 67e974c..fc8f44d 100644 --- a/src/core/diff.rs +++ b/src/core/diff.rs @@ -6,11 +6,6 @@ use std::path::Path; use tracing::{error, info, warn}; pub fn run_diffs(config: &LumenConfig) -> Result<(), Box> { - info!( - "🔍 Running visual diff with threshold: {}", - config.threshold - ); - let snapshots_dir = Path::new(".lumendiff/snapshots"); let baseline_dir = Path::new(".lumendiff/baseline"); let diffs_dir = Path::new(".lumendiff/diffs"); @@ -28,6 +23,26 @@ pub fn run_diffs(config: &LumenConfig) -> Result<(), Box> return Ok(()); } + if config.update { + info!("🔄 Updating baselines from snapshots..."); + let updated: usize = entries + .par_iter() + .filter_map(|entry| { + let snapshot_path = entry.path(); + let filename = snapshot_path.file_name().unwrap(); + let baseline_path = baseline_dir.join(filename); + fs::copy(&snapshot_path, &baseline_path).ok().map(|_| 1) + }) + .sum(); + info!("✅ Updated {} baselines", updated); + return Ok(()); + } + + info!( + "🔍 Running visual diff with threshold: {}", + config.threshold + ); + let results: Vec = entries .par_iter() .map(|entry| { @@ -37,11 +52,10 @@ pub fn run_diffs(config: &LumenConfig) -> Result<(), Box> let diff_path = diffs_dir.join(filename); if !baseline_path.exists() { - let message = format!( + warn!( "⚠️ No baseline found for {}, skipping diff", filename.to_string_lossy() ); - warn!(message); let _ = fs::copy(&snapshot_path, &baseline_path); return true; } @@ -90,10 +104,10 @@ pub fn run_diffs(config: &LumenConfig) -> Result<(), Box> }) .collect(); - let falhas = results.iter().filter(|&&passed| !passed).count(); + let failures = results.iter().filter(|&&passed| !passed).count(); - if falhas > 0 { - error!("❌ {} diffs found that exceed the threshold", falhas); + if failures > 0 { + error!("❌ {} diffs found that exceed the threshold", failures); error!("📁 Check the .lumendiff/diffs directory for details"); } else { info!("✅ All snapshots are within the acceptable threshold"); From 229575979f5c97b71b545afc362d67280fc2dda1 Mon Sep 17 00:00:00 2001 From: pmqueiroz Date: Sat, 25 Apr 2026 21:11:05 -0300 Subject: [PATCH 3/4] perf: eliminate double disk reads and skip diff image alloc on pass Two changes to the diff loop: - Use image::load_from_memory instead of image::open so PNG bytes already read for byte-comparison are reused, halving disk reads for non-identical pairs. - Two-pass pixel comparison: pass 1 counts differing pixels with no allocation; pass 2 builds the RGBA diff image only for stories that fail the threshold. Passing stories (the majority) skip ~5MB of buffer allocation and pixel work entirely. Co-Authored-By: Claude Sonnet 4.6 --- src/core/diff.rs | 127 ++++++++++++++++++++++++++--------------------- 1 file changed, 71 insertions(+), 56 deletions(-) diff --git a/src/core/diff.rs b/src/core/diff.rs index fc8f44d..b5f4086 100644 --- a/src/core/diff.rs +++ b/src/core/diff.rs @@ -60,47 +60,84 @@ pub fn run_diffs(config: &LumenConfig) -> Result<(), Box> return true; } - let snap_bytes = fs::read(&snapshot_path).unwrap_or_default(); - let base_bytes = fs::read(&baseline_path).unwrap_or_default(); + let snap_bytes = match fs::read(&snapshot_path) { + Ok(b) => b, + Err(e) => { + error!("❌ Failed to read snapshot {}: {}", filename.to_string_lossy(), e); + return false; + } + }; + let base_bytes = match fs::read(&baseline_path) { + Ok(b) => b, + Err(e) => { + error!("❌ Failed to read baseline {}: {}", filename.to_string_lossy(), e); + return false; + } + }; if snap_bytes == base_bytes { return true; } - let img_snapshot = image::open(&snapshot_path) - .expect("❌ Failed to open snapshot") - .into_rgba8(); - let img_baseline = image::open(&baseline_path) - .expect("❌ Failed to open baseline") - .into_rgba8(); - - match compare_images(&img_baseline, &img_snapshot) { - Ok((score, diff_image)) => { - let min_score_accepted = 1.0 - config.threshold; - - if score >= min_score_accepted { - true - } else { - error!( - "❌ {} differs from baseline (score: {:.4}), saving diff image", - filename.to_string_lossy(), - score - ); - diff_image - .save(&diff_path) - .expect("Failed to save diff image"); - false - } + // Decode from already-read bytes — avoids second disk read per image + let img_snapshot = match image::load_from_memory(&snap_bytes) { + Ok(img) => img.into_rgba8(), + Err(e) => { + error!("❌ Failed to decode snapshot {}: {}", filename.to_string_lossy(), e); + return false; } + }; + let img_baseline = match image::load_from_memory(&base_bytes) { + Ok(img) => img.into_rgba8(), Err(e) => { - error!( - "❌ Failed to compare images for {}: {}", - filename.to_string_lossy(), - e - ); - false + error!("❌ Failed to decode baseline {}: {}", filename.to_string_lossy(), e); + return false; } + }; + + let (width, height) = img_baseline.dimensions(); + if (width, height) != img_snapshot.dimensions() { + error!( + "❌ Dimension mismatch for {}: {}x{} vs {:?}", + filename.to_string_lossy(), + width, + height, + img_snapshot.dimensions() + ); + return false; + } + + let base_raw = img_baseline.as_raw(); + let snap_raw = img_snapshot.as_raw(); + let total_pixels = (width * height) as usize; + + // Pass 1: count-only, no allocation — skips diff image work for passing stories + let diff_pixels = base_raw + .chunks_exact(4) + .zip(snap_raw.chunks_exact(4)) + .filter(|(b, s)| b != s) + .count(); + + let similarity_score = 1.0 - (diff_pixels as f64 / total_pixels as f64); + let min_score_accepted = 1.0 - config.threshold; + + if similarity_score >= min_score_accepted { + return true; + } + + error!( + "❌ {} differs from baseline (score: {:.4}), saving diff image", + filename.to_string_lossy(), + similarity_score + ); + + // Pass 2: build diff image only for failing stories + let diff_image = build_diff_image(base_raw, snap_raw, width, height); + if let Err(e) = diff_image.save(&diff_path) { + error!("❌ Failed to save diff image for {}: {}", filename.to_string_lossy(), e); } + + false }) .collect(); @@ -116,23 +153,8 @@ pub fn run_diffs(config: &LumenConfig) -> Result<(), Box> Ok(()) } -fn compare_images(baseline: &RgbaImage, snapshot: &RgbaImage) -> Result<(f64, RgbaImage), String> { - let (width, height) = baseline.dimensions(); - - if (width, height) != snapshot.dimensions() { - return Err(format!( - "❌ Different dimensions: {}x{} vs {:?}", - width, - height, - snapshot.dimensions() - )); - } - - let base_raw = baseline.as_raw(); - let snap_raw = snapshot.as_raw(); - +fn build_diff_image(base_raw: &[u8], snap_raw: &[u8], width: u32, height: u32) -> RgbaImage { let mut diff_raw = vec![0u8; base_raw.len()]; - let mut diff_pixels = 0; for ((b_chunk, s_chunk), d_chunk) in base_raw .chunks_exact(4) @@ -145,7 +167,6 @@ fn compare_images(baseline: &RgbaImage, snapshot: &RgbaImage) -> Result<(f64, Rg d_chunk[2] = b_chunk[2]; d_chunk[3] = 75; } else { - diff_pixels += 1; d_chunk[0] = 255; d_chunk[1] = 0; d_chunk[2] = 0; @@ -153,11 +174,5 @@ fn compare_images(baseline: &RgbaImage, snapshot: &RgbaImage) -> Result<(f64, Rg } } - let total_pixels = (width * height) as usize; - let similarity_score = 1.0 - (diff_pixels as f64 / total_pixels as f64); - - let diff_image = - RgbaImage::from_raw(width, height, diff_raw).expect("❌ Failed to create diff image"); - - Ok((similarity_score, diff_image)) + RgbaImage::from_raw(width, height, diff_raw).expect("❌ Failed to create diff image") } From 99d3022fa20859ea3d370a8b480c354466ed5671 Mon Sep 17 00:00:00 2001 From: pmqueiroz Date: Sat, 25 Apr 2026 21:14:29 -0300 Subject: [PATCH 4/4] chore: remove inline comments from diff and main Co-Authored-By: Claude Sonnet 4.6 --- src/core/diff.rs | 3 --- src/main.rs | 1 - 2 files changed, 4 deletions(-) diff --git a/src/core/diff.rs b/src/core/diff.rs index b5f4086..b32dabb 100644 --- a/src/core/diff.rs +++ b/src/core/diff.rs @@ -79,7 +79,6 @@ pub fn run_diffs(config: &LumenConfig) -> Result<(), Box> return true; } - // Decode from already-read bytes — avoids second disk read per image let img_snapshot = match image::load_from_memory(&snap_bytes) { Ok(img) => img.into_rgba8(), Err(e) => { @@ -111,7 +110,6 @@ pub fn run_diffs(config: &LumenConfig) -> Result<(), Box> let snap_raw = img_snapshot.as_raw(); let total_pixels = (width * height) as usize; - // Pass 1: count-only, no allocation — skips diff image work for passing stories let diff_pixels = base_raw .chunks_exact(4) .zip(snap_raw.chunks_exact(4)) @@ -131,7 +129,6 @@ pub fn run_diffs(config: &LumenConfig) -> Result<(), Box> similarity_score ); - // Pass 2: build diff image only for failing stories let diff_image = build_diff_image(base_raw, snap_raw, width, height); if let Err(e) = diff_image.save(&diff_path) { error!("❌ Failed to save diff image for {}: {}", filename.to_string_lossy(), e); diff --git a/src/main.rs b/src/main.rs index f82c958..7dda305 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,7 +29,6 @@ async fn main() -> Result<(), Box> { } }); - // Probe server readiness instead of fixed sleep let probe_url = format!("http://localhost:{}/", port); for _ in 0..50 { if reqwest::get(&probe_url).await.is_ok() {