rusty_jpeg 0.1.1

Pure-Rust JPEG/MJPEG decoder + encoder, no C/FFI. Baseline and progressive DCT, planar YUV in/out, real quality and chroma-subsampling control.
Documentation
//! Feature-gated stage profiler — where the time goes, at zero cost when off.
//!
//! Behind the `profile` feature. With it off, [`scope`] returns a zero-sized
//! guard whose `Drop` is empty, so the optimizer removes it entirely and the
//! shipped build is byte-identical. With it on, each scope accumulates TSC
//! cycles into a per-stage bucket.
//!
//! # Reading a dump
//!
//! Read it top-down and look at the **residue** first — `Total` minus the sum of
//! the named stages. That is where unnamed work hides, and it is usually the
//! most informative line in the table.
//!
//! But a stubborn residue is not automatically work: every scope costs about two
//! `rdtsc` reads, so a stage entered a million times inflates both its own bucket
//! and the residue. Before chasing a residue, compute `calls x ~20ns` and compare.
//! If they match, you are measuring the instrument, and decomposing further will
//! not help.
//!
//! Percentages are what to trust. For absolute throughput, run with the feature
//! **off** and time the whole operation.

/// Pipeline stages worth timing separately.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(usize)]
pub enum Stage {
    /// Whole encode, wrapping everything below.
    Total = 0,
    /// Pulling source rows into per-component buffers (colour conversion for
    /// packed input; plane reads + chroma replication for planar).
    FillBuffers,
    /// Forward DCT.
    Fdct,
    /// Quantization.
    Quantize,
    /// Huffman symbol coding + bit writing.
    Entropy,
    /// The extra statistics pass that optimized Huffman tables require.
    HuffmanOptimize,
    /// Marker/header writing.
    Headers,
    /// Extracting one 8x8 block out of the row buffers (with chroma subsampling).
    GetBlock,
    /// Per-block-row buffer management: clears, edge padding, allocation.
    RowSetup,
}

impl Stage {
    pub const COUNT: usize = 9;
    pub fn name(self) -> &'static str {
        match self {
            Stage::Total => "Total",
            Stage::FillBuffers => "FillBuffers",
            Stage::Fdct => "Fdct",
            Stage::Quantize => "Quantize",
            Stage::Entropy => "Entropy",
            Stage::HuffmanOptimize => "HuffmanOptimize",
            Stage::Headers => "Headers",
            Stage::GetBlock => "GetBlock",
            Stage::RowSetup => "RowSetup",
        }
    }
    fn from_index(i: usize) -> Stage {
        match i {
            0 => Stage::Total,
            1 => Stage::FillBuffers,
            2 => Stage::Fdct,
            3 => Stage::Quantize,
            4 => Stage::Entropy,
            5 => Stage::HuffmanOptimize,
            6 => Stage::Headers,
            7 => Stage::GetBlock,
            _ => Stage::RowSetup,
        }
    }
}

#[cfg(not(feature = "profile"))]
mod imp {
    use super::Stage;

    /// Zero-sized no-op guard. `Drop` is empty, so this compiles away.
    pub struct Guard;

    #[inline(always)]
    pub fn scope(_stage: Stage) -> Guard {
        Guard
    }

    #[inline(always)]
    pub fn reset() {}

    pub fn snapshot() -> [(f64, u64); Stage::COUNT] {
        [(0.0, 0); Stage::COUNT]
    }

    pub fn dump() -> alloc::string::String {
        alloc::string::String::from(
            "profiling disabled — rebuild with `--features profile` to get a breakdown\n",
        )
    }
}

#[cfg(feature = "profile")]
mod imp {
    use super::Stage;
    use alloc::format;
    use alloc::string::String;
    use core::sync::atomic::{AtomicU64, Ordering};

    #[allow(clippy::declare_interior_mutable_const)]
    const ZERO: AtomicU64 = AtomicU64::new(0);
    static CYCLES: [AtomicU64; Stage::COUNT] = [ZERO; Stage::COUNT];
    static CALLS: [AtomicU64; Stage::COUNT] = [ZERO; Stage::COUNT];

    /// `rdtsc` (~15 ns) rather than `Instant::now()` (~30 ns on Windows, where it
    /// is `QueryPerformanceCounter`). At these call counts the timer *is* the
    /// overhead, so halving it materially shrinks the phantom residue.
    #[inline(always)]
    fn now() -> u64 {
        #[cfg(all(
            feature = "profile",
            any(target_arch = "x86", target_arch = "x86_64")
        ))]
        {
            // SAFETY: `_rdtsc` is a plain register read with no memory operands
            // and no preconditions. Only ever compiled into the dev-only
            // profiling build.
            #[allow(unsafe_code)]
            unsafe {
                #[cfg(target_arch = "x86_64")]
                use core::arch::x86_64::_rdtsc;
                #[cfg(target_arch = "x86")]
                use core::arch::x86::_rdtsc;
                _rdtsc()
            }
        }
        #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
        {
            0
        }
    }

    pub struct Guard {
        stage: usize,
        start: u64,
    }

    impl Drop for Guard {
        #[inline(always)]
        fn drop(&mut self) {
            let elapsed = now().wrapping_sub(self.start);
            CYCLES[self.stage].fetch_add(elapsed, Ordering::Relaxed);
            CALLS[self.stage].fetch_add(1, Ordering::Relaxed);
        }
    }

    #[inline(always)]
    pub fn scope(stage: Stage) -> Guard {
        Guard {
            stage: stage as usize,
            start: now(),
        }
    }

    pub fn reset() {
        for i in 0..Stage::COUNT {
            CYCLES[i].store(0, Ordering::Relaxed);
            CALLS[i].store(0, Ordering::Relaxed);
        }
    }

    /// `(cycles, calls)` per stage — raw, so callers can take medians over runs.
    pub fn snapshot() -> [(f64, u64); Stage::COUNT] {
        let mut out = [(0.0, 0); Stage::COUNT];
        for i in 0..Stage::COUNT {
            out[i] = (
                CYCLES[i].load(Ordering::Relaxed) as f64,
                CALLS[i].load(Ordering::Relaxed),
            );
        }
        out
    }

    pub fn dump() -> String {
        let snap = snapshot();
        let total = snap[Stage::Total as usize].0.max(1.0);
        let mut s = String::from("stage             cycles%      Mcycles      calls\n");
        let mut named = 0.0;
        for i in 1..Stage::COUNT {
            let (cy, calls) = snap[i];
            named += cy;
            s.push_str(&format!(
                "{:<16} {:>7.2}% {:>12.1} {:>10}\n",
                Stage::from_index(i).name(),
                100.0 * cy / total,
                cy / 1e6,
                calls
            ));
        }
        let residue = total - named;
        let scopes: u64 = snap.iter().map(|(_, c)| *c).sum();
        s.push_str(&format!(
            "{:<16} {:>7.2}% {:>12.1} {:>10}\n",
            "residue",
            100.0 * residue / total,
            residue / 1e6,
            "-"
        ));
        s.push_str(&format!(
            "{:<16} {:>7.2}% {:>12.1} {:>10}\n",
            "Total",
            100.0,
            total / 1e6,
            snap[Stage::Total as usize].1
        ));
        // The instrument's own cost, so a residue can be judged rather than chased.
        s.push_str(&format!(
            "\n{} scope entries; at ~40 cycles/scope the probe itself accounts for \
             ~{:.1} Mcycles ({:.1}% of Total).\n\
             If that is close to the residue, the residue IS the instrument — stop decomposing.\n",
            scopes,
            (scopes * 40) as f64 / 1e6,
            100.0 * (scopes * 40) as f64 / total
        ));
        s
    }
}

pub use imp::{dump, reset, scope, snapshot, Guard};