Packages

A high-performance, lossless compression library for time series data implementing Facebook's Gorilla compression algorithm.

Current section

Files

Jump to
gorilla_stream lib gorilla_stream stream.ex
Raw

lib/gorilla_stream/stream.ex

defmodule GorillaStream.Stream do
@moduledoc """
Streaming compression for very large datasets that don't fit in memory.
This module provides functions to compress/decompress data in chunks,
useful for processing massive time series datasets efficiently.
## Memory Efficiency
The default chunk size is 5,000 points (~78KB memory) which provides the
optimal balance between compression ratio and memory usage. Each chunk is
compressed independently, allowing true streaming with constant memory overhead.
## Container Compression
Supports optional container compression (zlib/zstd) via the `:compression` option:
- `:none` - No container compression (default, fastest)
- `:zlib` - Use zlib compression
- `:zstd` - Use zstd compression (requires ezstd package)
- `:auto` - Use zstd if available, fall back to zlib
## Examples
# Basic streaming compression
data_stream
|> GorillaStream.Stream.compress_stream()
|> Enum.each(&store_chunk/1)
# With zstd compression and smaller chunks
data_stream
|> GorillaStream.Stream.compress_stream(chunk_size: 500, compression: :zstd)
|> Enum.each(&store_chunk/1)
"""
alias GorillaStream.Compression.Gorilla.{Encoder, Decoder}
alias GorillaStream.Compression.Container
# Default to 5,000 points per chunk - optimal balance of compression vs memory
# At 16 bytes/point raw, this is ~78KB per chunk before compression
# Benchmarks show diminishing returns beyond this size
@default_chunk_size 5_000
@doc """
Compresses a stream of data in chunks.
## Options
- `:chunk_size` - Number of points per chunk (default: #{@default_chunk_size})
- `:compression` - Container compression (`:none`, `:zlib`, `:zstd`, `:auto`)
- `:victoria_metrics` - Enable VictoriaMetrics preprocessing (default: true)
- `:is_counter` - Treat data as counter (default: false)
- `:scale_decimals` - Decimal scaling (`:auto` or integer)
## Examples
iex> large_dataset
...> |> GorillaStream.Stream.compress_stream(chunk_size: 500, compression: :zstd)
...> |> Enum.to_list()
[{:ok, compressed_chunk_1, metadata_1}, {:ok, compressed_chunk_2, metadata_2}, ...]
"""
def compress_stream(data_stream, opts \\ []) do
chunk_size = Keyword.get(opts, :chunk_size, @default_chunk_size)
compression = Keyword.get(opts, :compression, :none)
# Extract encoder options
encoder_opts = Keyword.take(opts, [:victoria_metrics, :is_counter, :scale_decimals])
data_stream
|> Stream.chunk_every(chunk_size)
|> Stream.map(fn chunk ->
with {:ok, gorilla_compressed} <- Encoder.encode(chunk, encoder_opts),
{:ok, final_compressed} <-
Container.compress(gorilla_compressed, compression: compression) do
metadata = %{
original_points: length(chunk),
gorilla_size: byte_size(gorilla_compressed),
compressed_size: byte_size(final_compressed),
compression: compression,
timestamp_range: get_timestamp_range(chunk)
}
{:ok, final_compressed, metadata}
end
end)
end
@doc """
Decompresses a stream of compressed chunks.
## Options
- `:compression` - Container compression used (`:none`, `:zlib`, `:zstd`, `:auto`)
## Examples
compressed_chunks
|> GorillaStream.Stream.decompress_stream(compression: :zstd)
|> Stream.flat_map(fn {:ok, points} -> points end)
|> Enum.to_list()
"""
def decompress_stream(compressed_stream, opts \\ []) do
compression = Keyword.get(opts, :compression, :none)
compressed_stream
|> Stream.map(fn
{:ok, compressed, metadata} ->
# Use compression from metadata if available, otherwise from opts
comp = Map.get(metadata, :compression, compression)
decompress_chunk(compressed, comp)
{:ok, compressed} ->
decompress_chunk(compressed, compression)
error ->
error
end)
end
defp decompress_chunk(compressed, compression) do
with {:ok, gorilla_data} <- Container.decompress(compressed, compression: compression),
{:ok, points} <- Decoder.decode(gorilla_data) do
{:ok, points}
end
end
@doc """
Returns the default chunk size used for streaming.
"""
def default_chunk_size, do: @default_chunk_size
defp get_timestamp_range([]), do: {nil, nil}
defp get_timestamp_range([{first_ts, _} | _] = data) do
{last_ts, _} = List.last(data)
{first_ts, last_ts}
end
end