///|
/// Compression, for the question raw sizes cannot answer: what a module costs
/// once it is on the wire.
///
/// Servers serve WebAssembly gzip-compressed, and how much that saves depends
/// almost entirely on what the bytes look like. A name section full of symbols
/// and strings shrinks several times over; a data section full of already-packed
/// numbers barely moves. The raw section table therefore overstates what some
/// sections cost and understates others, and it is the compressed shares that
/// describe a transfer.
///|
/// Compress `data` as a gzip stream.
///
/// The compressor runs at its default level — six, which is what servers use —
/// so the result is what a transfer would pay rather than the best a compressor
/// could manage. It stays an estimate in two ways worth knowing about: a server
/// may be configured for a different level, and it compresses the response as a
/// whole rather than each section on its own.
pub fn compress_gzip(data : Bytes) -> Result[Bytes, String] {
Ok(@gzip.compress(data)) catch {
error => Err(error.to_string())
}
}
///|
/// The compressed size of `data` in bytes, or its raw size when it cannot be
/// compressed at all.
///
/// Falling back rather than failing keeps a report renderable: one stream the
/// compressor refuses should not take the run down, and claiming it shrank would
/// be worse than saying it did not.
pub fn compressed_size(data : Bytes) -> Int {
match compress_gzip(data) {
Ok(compressed) => compressed.length()
Err(_) => data.length()
}
}
///|
/// A section measured before and after compression.
pub struct SectionCompression {
label : String
raw : Int
gzip : Int
}
///|
/// Compress each section of `parsed` on its own, out of the file it was read
/// from, heaviest raw section first.
///
/// Each section becomes its own gzip stream, so each pays its own container —
/// roughly twenty bytes of magic, header, CRC and length. That is invisible for a
/// section of kilobytes and dominant for one of a handful of bytes, which is why
/// a tiny section can come back larger than it went in.
///
/// This is what `render_compression` is handed instead of bare `Section`s: a
/// `Section` records where its bytes are, not what they are, and a section
/// cannot be compressed without them.
pub fn compress_sections(
data : Bytes,
parsed : WasmModule,
) -> Array[SectionCompression] {
let rows : Array[SectionCompression] = []
for section in ordered_sections(parsed) {
let start = section.offset
let end = start + section.total_size
// A truncated file can claim a section that runs past its own end, so what
// is missing is measured as nothing rather than indexed out of bounds.
let bytes = if start >= 0 && start <= end && end <= data.length() {
data[start:end].to_owned()
} else {
Bytes::new(0)
}
rows.push({
label: section_label(section),
raw: section.total_size,
gzip: compressed_size(bytes),
})
}
rows
}
///|
/// The compressed size of `data[offset : offset + length]`, or `length` when that
/// range does not fit the file.
///
/// This is how a section or a function body is measured on its own: the caller
/// has the offsets the decoder recorded, not a slice of its own.
fn compressed_span(data : Bytes, offset : Int, length : Int) -> Int {
let end = offset + length
if offset < 0 || offset > end || end > data.length() {
return length
}
compressed_size(data[offset:end].to_owned())
}