Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/znippy

plugins/native/znippy-plugin-git/examples/explode_compress_ratio.rs

//! **What per-row compression of the eager exploded pool would buy, measured on a
//! real pool** — the number lever 3 needs before a byte of the writer changes.
//!
//! Arrow IPC's own body compression is per BUFFER, which kills the point read's
//! `pread` extent (one row cannot be lifted out of a compressed buffer), so the
//! candidate is compressing each row's payload on its own and inflating just
//! that row after the pread. This walks `objects.exploded` as the plain IPC
//! stream it is, compresses every payload with zlib (the codec git itself uses
//! per object, and already in this crate via `flate2`/zlib-rs — pure Rust),
//! and reports, per kind and per size class: rows, raw bytes, compressed bytes
//! at `fast` and `default`, and the inflate cost per row on a sample.
//!
//! Usage: `explode_compress_ratio <objects.exploded> [max_batches]`
use std::io::{Read, Write};
use std::time::Instant;

use anyhow::{Context, Result};
use znippy_common::arrow::array::{Array, LargeBinaryArray, UInt8Array};
use znippy_common::arrow::ipc::reader::StreamReader;

#[derive(Default, Clone, Copy)]
struct Acc {
    rows: u64,
    raw: u64,
    fast: u64,
    dflt: u64,
    /// Rows where `fast` did not reach 90 % of raw — compression not worth it.
    fast_useless: u64,
}

fn class(len: usize) -> usize {
    match len {
        0..=127 => 0,
        128..=511 => 1,
        512..=2047 => 2,
        2048..=8191 => 3,
        8192..=65535 => 4,
        _ => 5,
    }
}
const CLASS_NAMES: [&str; 6] = ["<128", "128-511", "512-2K", "2K-8K", "8K-64K", ">64K"];
const KIND_NAMES: [&str; 5] = ["?", "commit", "tree", "blob", "tag"];

fn zlib(data: &[u8], level: flate2::Compression) -> Vec<u8> {
    let mut e = flate2::write::ZlibEncoder::new(Vec::with_capacity(data.len() / 2 + 16), level);
    e.write_all(data).unwrap();
    e.finish().unwrap()
}

fn main() -> Result<()> {
    let mut args = std::env::args().skip(1);
    let path = args.next().context("usage: explode_compress_ratio <objects.exploded> [max_batches]")?;
    let max_batches: usize = args.next().map(|s| s.parse().unwrap_or(usize::MAX)).unwrap_or(usize::MAX);
    let f = std::fs::File::open(&path).with_context(|| format!("opening {path}"))?;
    let file_len = f.metadata()?.len();
    let reader = StreamReader::try_new(std::io::BufReader::with_capacity(8 << 20, f), None)
        .context("not an Arrow IPC stream")?;

    // [kind][class]
    let mut acc = [[Acc::default(); 6]; 5];
    let mut batches = 0usize;
    let mut sample: Vec<(Vec<u8>, usize)> = Vec::new(); // (compressed fast, raw len)
    let t0 = Instant::now();
    for batch in reader {
        let batch = batch?;
        let kinds = batch
            .column_by_name("object_type")
            .and_then(|c| c.as_any().downcast_ref::<UInt8Array>())
            .context("object_type column")?;
        let pay = batch
            .column_by_name("payload")
            .and_then(|c| c.as_any().downcast_ref::<LargeBinaryArray>())
            .context("payload column")?;
        for i in 0..batch.num_rows() {
            let k = (kinds.value(i) as usize).min(4);
            let p = pay.value(i);
            let c = class(p.len());
            let a = &mut acc[k][c];
            a.rows += 1;
            a.raw += p.len() as u64;
            let zf = zlib(p, flate2::Compression::fast());
            let zd = zlib(p, flate2::Compression::default());
            a.fast += zf.len() as u64;
            a.dflt += zd.len() as u64;
            if zf.len() * 10 > p.len() * 9 {
                a.fast_useless += 1;
            }
            if sample.len() < 20_000 && (i % 37 == 0) {
                sample.push((zf, p.len()));
            }
        }
        batches += 1;
        if batches >= max_batches {
            break;
        }
    }
    let walk = t0.elapsed();

    // Inflate cost per row, the read path's extra work.
    let t1 = Instant::now();
    let mut out = Vec::new();
    for (z, raw_len) in &sample {
        out.clear();
        let mut d = flate2::read::ZlibDecoder::new(z.as_slice());
        d.read_to_end(&mut out).unwrap();
        assert_eq!(out.len(), *raw_len);
    }
    let inflate = t1.elapsed();

    println!("file {file_len} B, {batches} batches walked in {:.1} s (includes two zlib passes per row)", walk.as_secs_f64());
    println!("inflate cost on {} sampled rows: {:.2} µs/row (mean)", sample.len(), inflate.as_secs_f64() * 1e6 / sample.len().max(1) as f64);
    println!();
    println!("{:<7} {:<8} {:>10} {:>14} {:>14} {:>6} {:>14} {:>6} {:>8}", "kind", "size", "rows", "raw", "zlib-fast", "×", "zlib-default", "×", "useless%");
    let mut tot = Acc::default();
    for k in 1..5 {
        for c in 0..6 {
            let a = acc[k][c];
            if a.rows == 0 { continue; }
            println!(
                "{:<7} {:<8} {:>10} {:>14} {:>14} {:>6.2} {:>14} {:>6.2} {:>7.1}%",
                KIND_NAMES[k], CLASS_NAMES[c], a.rows, a.raw, a.fast,
                a.raw as f64 / a.fast.max(1) as f64, a.dflt, a.raw as f64 / a.dflt.max(1) as f64,
                100.0 * a.fast_useless as f64 / a.rows as f64
            );
            tot.rows += a.rows; tot.raw += a.raw; tot.fast += a.fast; tot.dflt += a.dflt; tot.fast_useless += a.fast_useless;
        }
    }
    println!();
    println!(
        "TOTAL rows {} raw {} B; zlib-fast {} B ({:.2}×, saves {} B); zlib-default {} B ({:.2}×, saves {} B); rows where fast is useless {:.1}%",
        tot.rows, tot.raw, tot.fast, tot.raw as f64 / tot.fast.max(1) as f64, tot.raw.saturating_sub(tot.fast),
        tot.dflt, tot.raw as f64 / tot.dflt.max(1) as f64, tot.raw.saturating_sub(tot.dflt),
        100.0 * tot.fast_useless as f64 / tot.rows.max(1) as f64
    );
    Ok(())
}