Skip to content
Merged
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# ExCodecs

[![CI](https://github.com/thanos/codecs/actions/workflows/ci.yml/badge.svg)](https://github.com/thanos/codecs/actions/workflows/ci.yml)
[![Hex.pm](https://img.shields.io/hexpm/v/ex_codecs.svg)](https://hex.pm/packages/ex_codecs)
[![Hex.pm Downloads](https://img.shields.io/hexpm/dt/ex_codecs.svg)](https://hex.pm/packages/ex_codecs)
[![Documentation](https://img.shields.io/badge/docs-hex.pm-blue.svg)](https://hexdocs.pm/ex_codecs)
[![License](https://img.shields.io/hexpm/l/ex_codecs.svg)](https://github.com/thanos/codecs/blob/main/LICENSE)
[![Elixir](https://img.shields.io/badge/Elixir-%7E%3E%201.17-purple.svg)](https://elixir-lang.org)
[![Coverage Status](https://coveralls.io/repos/github/thanos/codecs/badge.svg?branch=main)](https://coveralls.io/github/thanos/codecs?branch=main)

An extensible BEAM-native codec framework for Elixir.

ExCodecs provides a unified API for compression, decompression, hashing, checksums,
Expand Down Expand Up @@ -127,7 +135,7 @@ Returns a structured `%ExCodecs.Codec{}` struct with metadata, or
| `:lz4` | compression | No | No | -- |
| `:snappy` | compression | No | No | -- |
| `:bzip2` | compression | Yes | No | `block_size` (1-9, default 9) |
| `:blosc2` | compression | Yes | Yes | `cname`, `clevel` (0-9, default 5), `shuffle` (`:none` / `:byte`, default `:byte`), `typesize` (default 8) |
| `:blosc2` | compression | Yes | Yes | `cname`, `clevel` (0-9, default 5), `shuffle` (`:none` / `:byte` / `:bit`, default `:byte`), `typesize` (default 8) |

## Architecture

Expand Down
8 changes: 1 addition & 7 deletions config/config.exs
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import Config

# In dev/test, build the NIF from source so `mix compile` and `mix test`
# work without precompiled artifacts. Release consumers use precompiled NIFs.
#
# CI and release workflows can also set EX_CODECS_BUILD=1 or
# RUSTLER_PRECOMPILED_FORCE_BUILD_ALL=1 to force source compilation
# (e.g. the publish_hex job in .github/workflows/release.yml).
if config_env() in [:dev, :test] do
config :rustler_precompiled, :force_build, ex_codecs: true
import_config "#{config_env()}.exs"
end
end
9 changes: 6 additions & 3 deletions lib/ex_codecs/compression/blosc2.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ defmodule ExCodecs.Compression.Blosc2 do
`:zstd`, `:snappy`, `:zlib` (default: `:lz4`)
* `:clevel` — Compression level 0-9 (default: 5). 0 means no compression
(store raw data).
* `:shuffle` — Shuffle filter: `:none`, `:byte` (default: `:byte`).
* `:shuffle` — Shuffle filter: `:none`, `:byte`, `:bit` (default: `:byte`).
Byte shuffle reorders data to improve compression ratios for typed data.
Bit shuffle transposes at the bit level for even better compression on numerical data.
* `:typesize` — Element size in bytes for shuffle (default: 8)

## Performance Characteristics

* Optimized for numerical/array data
* Byte shuffle dramatically improves compression ratios for typed data
* Bit shuffle can further improve compression on numerical data with small deltas
* Excellent when used with appropriate typesize and shuffle settings

## Examples
Expand All @@ -47,7 +49,7 @@ defmodule ExCodecs.Compression.Blosc2 do
@default_typesize 8

@valid_cnames [:lz4, :lz4hc, :blosclz, :zstd, :snappy, :zlib]
@valid_shuffles [:none, :byte]
@valid_shuffles [:none, :byte, :bit]

@doc """
Returns codec metadata for the registry.
Expand All @@ -73,7 +75,7 @@ defmodule ExCodecs.Compression.Blosc2 do

* `:cname` — Internal compressor atom (default: `:lz4`)
* `:clevel` — Compression level 0-9 (default: 5)
* `:shuffle` — Shuffle filter: `:none`, `:byte` (default: `:byte`)
* `:shuffle` — Shuffle filter: `:none`, `:byte`, `:bit` (default: `:byte`)
* `:typesize` — Element size in bytes (default: 8)
"""
@impl true
Expand Down Expand Up @@ -151,4 +153,5 @@ defmodule ExCodecs.Compression.Blosc2 do

defp shuffle_to_int(:none), do: 0
defp shuffle_to_int(:byte), do: 1
defp shuffle_to_int(:bit), do: 2
end
69 changes: 41 additions & 28 deletions livebooks/01_introduction.livemd
Original file line number Diff line number Diff line change
@@ -1,39 +1,52 @@
# Introduction to ExCodecs

```elixir
Mix.install([
{:ex_codecs, path: Path.join(__DIR__, "..")}
])
local_path = Path.join(__DIR__, "../mix.exs")

ex_codecs_dep =
if File.exists?(local_path) do
[{:ex_codecs, path: Path.join(__DIR__, "..")}, {:rustler, "~> 0.36"}]
else
[{:ex_codecs, "~> 0.1"}]
end

config =
if File.exists?(local_path) do
[rustler_precompiled: [force_build: [ex_codecs: true]]]
else
[]
end

Mix.install(ex_codecs_dep, config: config)
```

## What is a Codec?

A **codec** (coder-decoder) is an abstraction for transforming data between two representations. In ExCodecs, the two fundamental operations are:

- **Encode** — transform data into a compact or structured form
- **Decode** — recover the original data from the encoded form
* **Encode** — transform data into a compact or structured form
* **Decode** — recover the original data from the encoded form

For compression codecs, encoding means compressing and decoding means decompressing. But the codec abstraction extends beyond compression — it can represent hashing, checksums, binary encodings, and content-addressing transforms, all through the same `encode`/`decode` interface.

```elixir
# The universal codec interface
{:ok, encoded} = ExCodecs.encode(:some_codec, original_data)
{:ok, decoded} = ExCodecs.decode(:some_codec, encoded)

# Round-trip property: decoded == original_data
# The universal codec interface:
# {:ok, encoded} = ExCodecs.encode(:some_codec, data)
# {:ok, decoded} = ExCodecs.decode(:some_codec, encoded)
# decoded == data # round-trip guarantee
```

## Why Codecs Matter

Compression and encoding are fundamental to production systems:

| Concern | How Codecs Help |
|---|---|
| **Storage cost** | Compressed data uses less disk space |
| **Network bandwidth** | Smaller payloads mean faster transfers |
| **Data integrity** | Decode failures detect corruption |
| **Memory efficiency** | Compressed caches fit more in RAM |
| **Scientific data** | Blosc2 shuffle+compress slashes array sizes |
| Concern | How Codecs Help |
| --------------------- | ------------------------------------------- |
| **Storage cost** | Compressed data uses less disk space |
| **Network bandwidth** | Smaller payloads mean faster transfers |
| **Data integrity** | Decode failures detect corruption |
| **Memory efficiency** | Compressed caches fit more in RAM |
| **Scientific data** | Blosc2 shuffle+compress slashes array sizes |

A codec framework gives you a **single consistent API** over multiple algorithms, so you can choose the right tool per workload without rewriting integration code.

Expand Down Expand Up @@ -123,13 +136,13 @@ end

### Codec Feature Summary

| Codec | Category | Configurable | Streaming | Best For |
|---|---|---|---|---|
| `:zstd` | compression | Yes (level 1–22) | Yes | General-purpose, high ratio |
| `:lz4` | compression | Yes (level 1–16) | No | Real-time, low latency |
| `:snappy` | compression | No | No | Short-lived data, low overhead |
| `:bzip2` | compression | Yes (block_size 1–9) | No | Archival, maximum ratio |
| `:blosc2` | compression | Yes (many options) | Yes | Numerical/array data |
| Codec | Category | Configurable | Streaming | Best For |
| --------- | ----------- | ---------------------- | --------- | ------------------------------ |
| `:zstd` | compression | Yes (level 1–22) | Yes | General-purpose, high ratio |
| `:lz4` | compression | Yes (level 1–16) | No | Real-time, low latency |
| `:snappy` | compression | No | No | Short-lived data, low overhead |
| `:bzip2` | compression | Yes (block_size 1–9) | No | Archival, maximum ratio |
| `:blosc2` | compression | Yes (many options) | Yes | Numerical/array data |

## Error Handling

Expand All @@ -146,7 +159,7 @@ IO.puts("Reason: #{err.reason}")

## What's Next?

- **[Compression Fundamentals](02_compression_fundamentals.livemd)** — theory, trade-offs, and interactive benchmarks
- **[Codec Comparison](03_codec_comparison.livemd)** — side-by-side performance analysis
- **[Building Storage Systems](04_building_storage_systems.livemd)** — practical patterns for production use
- **[Zarr-Style Workloads](05_zarr_style_workloads.livemd)** — scientific dataset compression with Blosc2
* **[Compression Fundamentals](02_compression_fundamentals.livemd)** — theory, trade-offs, and interactive benchmarks
* **[Codec Comparison](03_codec_comparison.livemd)** — side-by-side performance analysis
* **[Building Storage Systems](04_building_storage_systems.livemd)** — practical patterns for production use
* **[Zarr-Style Workloads](05_zarr_style_workloads.livemd)** — scientific dataset compression with Blosc2
48 changes: 28 additions & 20 deletions livebooks/02_compression_fundamentals.livemd
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
# Compression Fundamentals

```elixir
Mix.install([
{:ex_codecs, path: Path.join(__DIR__, "..")},
{:kino, "~> 0.14"},
{:vega_lite, "~> 0.1"}
])
local_path = Path.join(__DIR__, "../mix.exs")

ex_codecs_dep =
if File.exists?(local_path) do
[{:ex_codecs, path: Path.join(__DIR__, "..")}, {:rustler, "~> 0.36"}]
else
[{:ex_codecs, "~> 0.1"}]
end

config =
if File.exists?(local_path) do
[rustler_precompiled: [force_build: [ex_codecs: true]]]
else
[]
end

Mix.install(
ex_codecs_dep ++ [{:jason, "~> 1.4"}, {:kino, "~> 0.14"}, {:kino_vega_lite, "~> 0.1.13"}],
config: config
)
```

## How Compression Works
Expand Down Expand Up @@ -103,19 +118,13 @@ IO.puts("Zstd (plain): #{byte_size(zstd_plain)} bytes")

```elixir
# Generate test datasets
datasets = %{
"JSON" => Jason.encode!(%{for i <- 1..500, do: %{id: i, name: "item_#{i}", value: :rand.uniform(1000)}}),
}

# For this demo, build JSON-like text manually
json_like = "[" <> Enum.join(for i <- 1..500 do
"{\"id\":#{i},\"name\":\"item_#{i}\",\"value\":#{:rand.uniform(1000)}}"
end, ",") <> "]"
json_like = Jason.encode!(for i <- 1..500, do: %{id: i, name: "item_#{i}", value: :rand.uniform(1000)})

datasets = %{
"Repetitive" => String.duplicate("abcdefghij", 5000),
"Natural text" => String.duplicate("The quick brown fox jumps over the lazy dog. ", 300),
"Semi-random" => for _ <- 1..10000, into: <<>>, do: <<:rand.uniform(255)>>
"Semi-random" => (for _ <- 1..10000, into: <<>>, do: <<:rand.uniform(255)>>),
"JSON-like" => json_like
}

results = for {label, data} <- datasets, codec <- [:lz4, :snappy, :zstd, :bzip2] do
Expand Down Expand Up @@ -146,7 +155,6 @@ VegaLite.new(width: 600, height: 400)
|> VegaLite.encode_field(:y, "ratio", type: :quantitative, title: "Compressed size (%)")
|> VegaLite.encode_field(:color, "dataset", type: :nominal)
|> VegaLite.encode_field(:column, "dataset", type: :nominal)
|> Kino.VegaLite.new()
```

### Speed Visualization
Expand All @@ -159,7 +167,6 @@ VegaLite.new(width: 600, height: 400)
|> VegaLite.encode_field(:y, "encode_us", type: :quantitative, title: "Encode time (µs)")
|> VegaLite.encode_field(:color, "dataset", type: :nominal)
|> VegaLite.encode_field(:column, "dataset", type: :nominal)
|> Kino.VegaLite.new()
```

## When Not to Compress
Expand All @@ -175,9 +182,10 @@ IO.puts("Compressed data can actually GROW due to header overhead")
```

Rules of thumb:
- **Don't compress encrypted or already-compressed data** — you waste CPU for no gain
- **Avoid compressing tiny payloads** — the codec header overhead may exceed savings
- **Consider latency** — LZ4/Snappy for hot paths, Bzip2 only for cold storage

* **Don't compress encrypted or already-compressed data** — you waste CPU for no gain
* **Avoid compressing tiny payloads** — the codec header overhead may exceed savings
* **Consider latency** — LZ4/Snappy for hot paths, Bzip2 only for cold storage

## Memory vs CPU Considerations

Expand Down Expand Up @@ -221,4 +229,4 @@ end
2. **No single best codec** — each excels for different data and latency requirements
3. **Shuffle transforms** (Blosc2) dramatically improve compression of typed binary data
4. **Compression level is a dial** — higher means smaller output but more CPU/memory
5. **Measure your actual data** — use the [Codec Comparison](03_codec_comparison.livemd) livebook with your own datasets
5. **Measure your actual data** — use the [Codec Comparison](03_codec_comparison.livemd) livebook with your own datasets
Loading
Loading