Skip to content

perf(gpui): halve 4K screenshot blur peak allocations with a row buffer - #3343

Draft
Reet24-del wants to merge 1 commit into
CapSoftware:mainfrom
Reet24-del:perf/screenshot-blur-row-buffer
Draft

Reet24-del wants to merge 1 commit into
CapSoftware:mainfrom
Reet24-del:perf/screenshot-blur-row-buffer

Conversation

@Reet24-del

@Reet24-del Reet24-del commented Oct 26, 2026 •

Copy link
Copy Markdown

Screenshot blur masks currently allocate every vertically filtered row before horizontally sampling them. Reuse one scratch row and immediately write its horizontally filtered pixels into the output. This preserves vertical-then-horizontal sampling, tap accumulation order, both horizontal paths and rounding, while reducing temporary allocations in GPUI screenshot previews and exports.

For a full 3840×2160 blur at level 16, measured peak live allocation requests during the complete filter drop from 65.503 MiB to 33.877 MiB (48.3% less). This includes the small intermediate image, final output and weight tables, but excludes the preallocated input. It is not process RSS. The two 31.64 MiB vertical scratch allocations run sequentially; their replacements are 60 KiB and 15 KiB.

This follows merged #2979, using its implementation as the baseline. Unchanged masks are already cached; the saving applies when a blur mask actually needs filtering.

Measurements

Apple M5, 16 GB RAM, macOS 26.5.2, arm64; Rust 1.88.0, image 0.25.10, opt-level 3, thin LTO and one codegen unit. Baseline fb453e3e3032f8b976378cd477e676eeb32f7bbc, candidate 1d4067138213c531a276f564c35edd0e3a222ce9. The baseline screenshot source is identical to the original investigation commit f6046f1ec82314b0aa887b76bc1ebf8bc77e3579.

Five independent process runs. Each workload asserts full output-image equality, discards three warmup pairs and records 21 samples with alternating execution order. Timings include the complete filter and output destruction, but exclude fixture creation and equality assertions. Tracking is disabled during timings; a separate allocator probe records peak requested live bytes for each implementation and verifies that all tracked bytes are freed. The table reports medians of the five process medians and the median of the five paired speedup ratios.

Region Level Before, ms After, ms Median paired speedup Before peak, MiB After peak, MiB
32×24 8 0.0040 0.0040 1.010× 0.012 0.006
32×24 16 0.0033 0.0033 1.000× 0.008 0.005
32×24 40 0.0029 0.0029 1.000× 0.006 0.005
400×300 8 0.3688 0.3565 1.035× 1.516 0.604
400×300 16 0.2849 0.2783 1.024× 0.973 0.516
400×300 40 0.2436 0.2400 1.013× 0.674 0.491
1920×1080 8 6.2867 6.1063 1.028× 25.830 10.024
1920×1080 16 4.8685 4.7505 1.027× 16.437 8.534
1920×1080 40 4.1019 4.0929 1.007× 11.275 8.114
3840×2160 8 25.6579 24.6443 1.041× 103.076 39.824
3840×2160 16 19.5157 19.1301 1.019× 65.503 33.877
3840×2160 40 16.5826 16.3235 1.012× 44.857 32.207

Memory reduction is the main result. Timing improvements are modest. Tiny masks are near timing noise; one 1080p/level-40 process regressed by about 1.2%, while its median paired result was 1.007×. No native UI responsiveness or whole-app RSS improvement is claimed.

Validation and remaining checks

  • Isolated release tests: 27 passed (13 existing screenshot tests on each source version, plus one direct before/after differential test across dimensions, subregions, empty regions and levels).
  • Every timed workload also checks byte-for-byte image equality, including full 4K masks.
  • Isolated release Clippy with all targets and warnings denied passed; touched-source rustfmt and git diff whitespace checks passed.
  • Native setup is now prepared: the exact pinned Zed/GPUI checkout and all eight Cap patches, Rust 1.95.0, native build tools, FFmpeg and ONNX Runtime. A full locked GPUI build progressed into native dependencies, then failed in cidre because xcodebuild requires full Xcode; this Mac currently has Command Line Tools only. The app has not been built or launched. The actual changed source passed the 27 isolated tests on both Rust 1.88.0 and 1.95.0 with image 0.25.10; this remains distinct from full-workspace/native validation.
  • Workspace formatting check reports existing differences in unrelated files; those files were left alone.
  • Keep as draft pending full-workspace/native checks. The GPUI app has not been launched; screenshot preview, save/copy and native integration remain unverified.
Standalone before/after benchmark source and reproduction commands

Run from a checkout containing both revisions. These commands pin image 0.25.10 but resolve transitive dependencies afresh. The exact measured image dependency versions were: adler2 2.0.1, autocfg 1.5.1, bitflags 2.13.2, bytemuck 1.25.2, byteorder-lite 0.1.0, cfg-if 1.0.5, crc32fast 1.5.3, fdeflate 0.3.7, flate2 1.1.10, image 0.25.10, image-webp 0.2.4, miniz_oxide 0.8.9, miniz_oxide 0.9.1, moxcms 0.8.1, num-traits 0.2.19, png 0.18.1, pxfm 0.1.30, quick-error 2.0.1, simd-adler32 0.3.10, zlib-rs 0.6.8, zune-core 0.5.3, zune-jpeg 0.5.15.

mkdir -p /tmp/cap-blur-repro/src
git show fb453e3e3032f8b976378cd477e676eeb32f7bbc:apps/desktop-gpui/kernels/src/screenshot.rs > /tmp/cap-blur-repro/src/baseline.rs
git show 1d4067138213c531a276f564c35edd0e3a222ce9:apps/desktop-gpui/kernels/src/screenshot.rs > /tmp/cap-blur-repro/src/candidate.rs
cat > /tmp/cap-blur-repro/Cargo.toml <<'MANIFEST'
[package]
name = "cap-blur-row-benchmark"
version = "0.1.0"
edition = "2024"

[dependencies]
image = { version = "=0.25.10", default-features = false, features = ["jpeg", "png", "webp"] }

[profile.release]
codegen-units = 1
lto = "thin"

[lints.rust]
dead_code = "allow"
MANIFEST
cat > /tmp/cap-blur-repro/src/main.rs <<'RUST'
mod baseline;
mod candidate;

use std::alloc::{GlobalAlloc, Layout, System};
use std::hint::black_box;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed};
use std::time::Instant;

struct Tracking;
static TRACK: AtomicBool = AtomicBool::new(false);
static LIVE: AtomicUsize = AtomicUsize::new(0);
static PEAK: AtomicUsize = AtomicUsize::new(0);

unsafe impl GlobalAlloc for Tracking {
    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
        let ptr = unsafe { System.alloc(layout) };
        if !ptr.is_null() && TRACK.load(Relaxed) {
            let live = LIVE.fetch_add(layout.size(), Relaxed) + layout.size();
            PEAK.fetch_max(live, Relaxed);
        }
        ptr
    }
    unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
        let ptr = unsafe { System.alloc_zeroed(layout) };
        if !ptr.is_null() && TRACK.load(Relaxed) {
            let live = LIVE.fetch_add(layout.size(), Relaxed) + layout.size();
            PEAK.fetch_max(live, Relaxed);
        }
        ptr
    }
    unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
        let next = unsafe { System.realloc(ptr, layout, new_size) };
        if !next.is_null() && TRACK.load(Relaxed) {
            let live = if new_size >= layout.size() {
                LIVE.fetch_add(new_size - layout.size(), Relaxed) + new_size - layout.size()
            } else {
                LIVE.fetch_sub(layout.size() - new_size, Relaxed) - (layout.size() - new_size)
            };
            PEAK.fetch_max(live, Relaxed);
        }
        next
    }
    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
        if TRACK.load(Relaxed) {
            LIVE.fetch_sub(layout.size(), Relaxed);
        }
        unsafe { System.dealloc(ptr, layout) };
    }
}

#[global_allocator]
static ALLOCATOR: Tracking = Tracking;

fn run(input: &[u8], w: u32, h: u32, level: f64, after: bool) -> image::RgbaImage {
    let filter = if after {
        candidate::mask_filter
    } else {
        baseline::mask_filter
    };
    filter(black_box(input), w, h, (0, 0, w, h), false, level).unwrap()
}

fn peak(input: &[u8], w: u32, h: u32, level: f64, after: bool) -> usize {
    LIVE.store(0, Relaxed);
    PEAK.store(0, Relaxed);
    TRACK.store(true, Relaxed);
    drop(black_box(run(input, w, h, level, after)));
    TRACK.store(false, Relaxed);
    assert_eq!(LIVE.load(Relaxed), 0);
    PEAK.load(Relaxed)
}

fn time(input: &[u8], w: u32, h: u32, level: f64, after: bool) -> u128 {
    let start = Instant::now();
    drop(black_box(run(input, w, h, level, after)));
    start.elapsed().as_nanos()
}

fn main() {
    println!("width,height,level,before_ns,after_ns,speedup,before_peak_bytes,after_peak_bytes");
    for (w, h) in [(32, 24), (400, 300), (1920, 1080), (3840, 2160)] {
        let input: Vec<u8> = (0..w as usize * h as usize * 4)
            .map(|i| (i.wrapping_mul(31).wrapping_add(i / 17) % 256) as u8)
            .collect();
        for level in [8., 16., 40.] {
            assert_eq!(
                run(&input, w, h, level, false),
                run(&input, w, h, level, true)
            );
            let before_peak = peak(&input, w, h, level, false);
            let after_peak = peak(&input, w, h, level, true);
            for _ in 0..3 {
                black_box(time(&input, w, h, level, false));
                black_box(time(&input, w, h, level, true));
            }
            let mut before = Vec::new();
            let mut after = Vec::new();
            for sample in 0..21 {
                if sample % 2 == 0 {
                    before.push(time(&input, w, h, level, false));
                    after.push(time(&input, w, h, level, true));
                } else {
                    after.push(time(&input, w, h, level, true));
                    before.push(time(&input, w, h, level, false));
                }
            }
            before.sort_unstable();
            after.sort_unstable();
            println!(
                "{w},{h},{level},{},{},{:.3},{before_peak},{after_peak}",
                before[10],
                after[10],
                before[10] as f64 / after[10] as f64
            );
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn blur_matches_current_main_across_regions_and_levels() {
        for (w, h) in [(1, 1), (1, 33), (33, 1), (7, 9), (63, 47), (128, 97)] {
            let input: Vec<u8> = (0..w as usize * h as usize * 4)
                .map(|i| (i.wrapping_mul(73).wrapping_add(i / 7) % 256) as u8)
                .collect();
            for region in [
                (0, 0, w, h),
                (w / 2, h / 2, w - w / 2, h - h / 2),
                (0, 0, 0, 0),
            ] {
                for level in [0., 0.5, 1., 8., 16., 40., 100., 1000.] {
                    let expected = baseline::mask_filter(&input, w, h, region, false, level);
                    let actual = candidate::mask_filter(&input, w, h, region, false, level);
                    assert_eq!(actual, expected, "{w}x{h} {region:?} level {level}");
                }
            }
        }
    }
}
RUST
cargo +1.88.0 test --release --manifest-path /tmp/cap-blur-repro/Cargo.toml
cargo +1.88.0 run --release --manifest-path /tmp/cap-blur-repro/Cargo.toml

@vercel

vercel Bot commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

@Reet24-del is attempting to deploy a commit to the Cap Software Inc Team on Vercel.

A member of the Team first needs to authorize it.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant