Current section
Files
Jump to
Current section
Files
livebooks/05_zarr_style_workloads.livemd
# Zarr-Style Workloads
```elixir
# Use this install to work with the source code
# Mix.install(
# [{:ex_codecs, path: Path.join(__DIR__, "..")}],
# config: [rustler_precompiled: [force_build: [ex_codecs: true]]]
# )
Mix.install([{:ex_codecs, "~> 0.2.3"}])
```
## Series
| # | Livebook |
| --- | -------------------------------------------------------------- |
| 01 | [Introduction](01_introduction.livemd) |
| 02 | [Compression Fundamentals](02_compression_fundamentals.livemd) |
| 03 | [Codec Comparison](03_codec_comparison.livemd) |
| 04 | [Building Storage Systems](04_building_storage_systems.livemd) |
| 05 | **Zarr-Style Workloads** (this) |
| 06 | [Spatial Codecs](06_spatial_codecs.livemd) |
## Scientific Dataset Compression
This notebook demonstrates compression patterns that array stores such as
[Zarr](https://zarr.dev/) use, optimized for numerical and scientific data.
## Array Data Layout
Scientific datasets are typically stored as multi-dimensional arrays with
strong regularity:
* Numeric types (integers, floats) with fixed element sizes
* Regular dimension strides
* Column-oriented or row-oriented access patterns
Blosc2 was specifically designed for this workload:
```elixir
# Generate a synthetic dataset (64-bit float array)
n_elements = 100_000
data = :binary.copy(<<3.14159265::float-64>>, n_elements)
data_size = byte_size(data)
# Compare codecs on regular numeric data
codecs = [:zstd, :lz4, :snappy, :bzip2, :blosc2]
results =
for codec <- codecs do
{time, {:ok, compressed}} = :timer.tc(fn -> ExCodecs.encode(codec, data) end)
ratio = Float.round(data_size / byte_size(compressed), 2)
{codec, byte_size: byte_size(compressed), ratio: ratio, time_us: time}
end
IO.puts("Codec | Compressed | Ratio | Time (us)")
IO.puts("---------|-----------|-------|----------")
for {codec, opts} <- results do
IO.puts("#{String.pad_trailing("#{codec}", 8)} | #{String.pad_trailing("#{opts[:byte_size]}", 9)} | #{opts[:ratio]} | #{opts[:time_us]}")
end
```
## Blosc2 Shuffle for Numerical Data
The shuffle filter is the key to Blosc2's effectiveness on array data:
```elixir
data = :binary.copy(<<1.0::float-64>>, 50_000) <> :binary.copy(<<2.0::float-64>>, 50_000)
# No shuffle
{:ok, c_none} = ExCodecs.encode(:blosc2, data, typesize: 8, shuffle: :none)
# Byte shuffle - reorders bytes for better compression
{:ok, c_byte} = ExCodecs.encode(:blosc2, data, typesize: 8, shuffle: :byte)
# Bit shuffle - reorders bits for even better compression on some data
{:ok, c_bit} = ExCodecs.encode(:blosc2, data, typesize: 8, shuffle: :bit)
IO.puts("No shuffle: #{byte_size(c_none)} bytes")
IO.puts("Byte shuffle: #{byte_size(c_byte)} bytes")
IO.puts("Bit shuffle: #{byte_size(c_bit)} bytes")
```
## Chunk-Based Compression
Large datasets are typically split into chunks, each compressed independently:
```elixir
chunk_size = 1024 * 8 # 8 KiB chunks
large_data = :binary.copy(<<3.14159265::float-64>>, 131_072) # 1 MiB of compressible floats
chunks =
for <<chunk::binary-size(chunk_size) <- large_data>>, do: chunk
compressed_chunks =
chunks
|> Enum.map(fn chunk ->
{:ok, compressed} = ExCodecs.encode(:zstd, chunk, level: 3)
compressed
end)
total_original = byte_size(large_data)
total_compressed = compressed_chunks |> Enum.map(&byte_size/1) |> Enum.sum()
IO.puts("Original: #{total_original} bytes")
IO.puts("Compressed: #{total_compressed} bytes")
IO.puts("Ratio: #{Float.round(total_original / total_compressed, 2)}x")
```
## Decompressing Individual Chunks
Only the needed chunks need to be decompressed:
```elixir
# Decompress just chunk 42
target = Enum.at(compressed_chunks, 42)
{:ok, decompressed} = ExCodecs.decode(:zstd, target)
IO.puts("Decompressed chunk size: #{byte_size(decompressed)} bytes")
IO.puts("Matches original: #{decompressed == Enum.at(chunks, 42)}")
```
## Choosing the Right Codec for Your Data
```elixir
data_patterns = %{
"Repetitive" => String.duplicate("AAAA", 100_000),
"Numeric array" => :binary.copy(<<3.14159::float-64>>, 100_000),
"Mixed" => (:crypto.strong_rand_bytes(500) <> String.duplicate("X", 500)) |> String.duplicate(100),
"Random" => :crypto.strong_rand_bytes(100_000)
}
codecs = [:zstd, :lz4, :snappy, :bzip2, :blosc2]
IO.puts(String.pad_trailing("Pattern", 15) <> " | " <> Enum.join(codecs, " | "))
for {name, data} <- data_patterns do
ratios =
for codec <- codecs do
{:ok, c} = ExCodecs.encode(codec, data)
Float.round(byte_size(data) / byte_size(c), 2)
end
ratio_strs = Enum.map(ratios, &String.pad_trailing("#{&1}", 5))
IO.puts(String.pad_trailing(name, 15) <> " | " <> Enum.join(ratio_strs, " | "))
end
```
## Key Takeaways
1. **Blosc2 with byte shuffle** excels on regular numeric arrays
2. **Zstd** is the best general-purpose codec
3. **LZ4 and Snappy** are fastest for real-time compression
4. **Bzip2** gives the best ratios for archival
5. **Chunk-based compression** enables random access to large datasets
## Navigation
**Previous:** [Building Storage Systems](04_building_storage_systems.livemd) ·
**Next:** [Spatial Codecs](06_spatial_codecs.livemd)