A year of household power
521,669 one-minute readings from uci-power-active-2007, in bits per point.
13.9 times smaller than 16 bytes a point, and 2.5 times smaller than zstd on the raw columns.
A Rust library and command-line tool that compresses time series of (i64, f64) points without losing a single bit. For each block of 4,096 points it measures Gorilla, Chimp, Chimp128 and a decimal codec, and writes whichever is smallest.
Gorilla, the usual starting point, XORs each value with the one before and stores the bits that changed. That works when neighbouring floats share most of their bits. Most metrics reach a database as text, though: a tide gauge prints 1.327, and parsed to a double that is a repeating binary fraction. On five of the six real series here, Gorilla comes out bigger than gzip of the CSV. tsz checks, bit for bit, whether a block's values were printed decimals, and if so stores the integers.
Plays the seven stages below in order, about 65 seconds. Scroll or press Esc to stop.
cargo test, plus a million-series soakauto per blockReal runs of the release binary, output verbatim. The CSV files are the repository's six datasets, unzipped from data/. The values in the last two tabs were written for this page Illustrative input.
tsz encode reads one timestamp,value per line, from a file or stdin. Timestamps are integers in any unit. A value is anything Rust's f64 parser accepts, or 0x and 16 hex digits for an exact bit pattern.
-0 keeps its sign, and a NaN keeps its payload.tsz decode --bits prints the 0x form back, so even a NaN payload survives a trip through text.Six values in, six bit patterns out Illustrative input
tsz encode and decoded both ways; every pattern is the decoder's own output. The bad files are a few lines each, made to trip the parser. The errors and exit codes are real.Gorilla, from Facebook's in-memory time-series database, XORs each value with the previous one. A repeat costs one bit. Otherwise it stores the run of bits between the leading and trailing zeros, reusing the last window when the new bits fit inside it.
i64::MIN can sit next to i64::MAX.tsz crate's size on five datasets is some evidence that both do.A printed decimal is the problem. 1.327 and 1.297 differ in a few digits, but as doubles their XOR has bits set almost all the way down.
The first ten values of a real series, as Gorilla sees them
data/noaa-sf-water-level.csv and noaa-sf-air-temp.csv. The bits per value come from a line-by-line port of the crate's Gorilla value coder, checked against the binary: for the first block of 4,096 points both give exactly the byte counts that tsz encode --codec gorilla printed.Chimp (VLDB 2022) noticed that XORs of real values rarely have many trailing zeros. It pays for a trailing-zero count only when there are more than six, and rounds the leading-zero count to one of eight values so it fits in three bits.
On the Wikipedia counts Gorilla beats both. They are integers stored as floats, so their XORs end in long runs of zeros, and Chimp pays 11 bits of header where Gorilla usually reuses its window for 2.
Bits per point on six real series, against gzip of the text
tsz compare data/*.csv, and the csv.gz marks from the bench's size table, which this page re-ran: both match the README to the last digit. Bits per point counts the timestamp and the value together; uncompressed is 128.For d from 0 to 15, the encoder tries n = round(v × 10d) on every value in the block. It keeps the first d for which n / 10d is the same 64 bits as v. Division and parsing are both correctly rounded, so 1327 / 1000 is exactly the double that "1.327" parses to.
-0.0 and NaN never match, and neither does a computed value such as 0.1 + 0.2.One value that doesn't fit sends the whole block to a float codec. There are no per-value exceptions, which pco and ALP have and this doesn't.
The same water levels, stored as integers
Break one value in a block of power readings Illustrative input
uci-power-active-2007.csv with line 10 changed; the lines are tsz compare output.Every encoder is generic over a bit sink. auto runs each candidate into a counter that only adds up lengths, and writes only the cheapest. Blocks are independent, so each one chooses its timestamp codec and its value codec on its own.
auto from about 17 to about 26 million points a second on encode, by the README's measurement.auto chooses from a superset of what each fixed codec uses, it is never bigger than any of them. The property tests check that on every random series.One file, three blocks, three choices Illustrative input
tsz compare on that block alone; the terminal is tsz inspect on the whole file encoded with auto.Bit for bitAbove this line tsz chooses how to store each block. Below it, what comes back must be the same 64 bits that went in, and bytes from anywhere are read as if they were hostile.
The format is a magic number, then blocks of a point count, one codec byte and two length-prefixed bit streams, then a zero. The decoder checks every one of those against what the encoder could have written, and a mismatch is an error, never a panic.
There are no checksums. A flipped bit that keeps the structure intact decodes to wrong values with no error, so wrap the stream in something with a CRC if that matters.
Seven damaged streams
auto to 5,373 bytes and then damaged as described; huge.tsz is the header the test suite uses. Messages and exit codes are what tsz decode printed. For the flipped bit, the values were compared bit for bit with the original: every timestamp survived, and 1,925 of the 5,000 values did not.tsz verify encodes a file under every codec, decodes it, and compares each value bit for bit. It exits 1 on any mismatch. The test suite does the same on series built to break float handling.
i64::MIN and MAX; lengths of 0, 1, 2 and a block plus or minus one.cargo test runs 20,000 random series. The soak in CI runs a million, and checks on each that auto is no bigger than the best fixed codec.Six datasets, four codecs, 1.3 million points
tsz verify data/*.csv on the release binary, then make soak, both verbatim. The soak took 156 s here, on a machine running other jobs, and 97 s in CI.The repository benchmarks tsz against zstd, the existing tsz crate and pco, and CI fails if the chart drifts from what the code produces. This page re-ran the size table: every figure matched the README, and the chart file came out byte-identical.
521,669 one-minute readings from uci-power-active-2007, in bits per point.
13.9 times smaller than 16 bytes a point, and 2.5 times smaller than zstd on the raw columns.
tsz auto against pco 1.0.4 at its default level, bits per point:
pco also notices the decimal step, then entropy-codes over bins fitted to the whole chunk. If data can go over in chunks, the README says to use pco.
cargo test, warm, twice in a row on this machine:
Most of it is the 20,000 random series. CI runs the suite on Rust 1.74 and stable, rustfmt and clippy with warnings as errors, the million-series soak, two minutes of each fuzz target, and the size chart check.
A two-minute fuzz run of decode here made 13.1 million attempts and found nothing.
Counted with cargo test --locked, as CI runs it: 21 passing across unit tests, three integration files and one doc test. The soak is a 22nd test, ignored by default and run with make soak; it passed here too.
encode.decode | head exiting quietly instead of panicking.While building it, nine bugs were planted by hand, one at a time, and the README records that eight made the suite fail. The ninth turned out to change no output at all.
n / 10d sends the whole block to a float codec.(i64, f64): no integer or string values, no columns or tags. Nothing rounds, quantizes or drops points.tsz-codec and lives on GitHub. tsz on crates.io is the older Gorilla crate it is compared with.git clone https://github.com/ahmed-hashim-pro/tsz.git && cd tsz
cargo build --release
export PATH=$PWD/target/release:$PATH
gunzip -k data/*.csv.gz
tsz compare data/*.csv
tsz encode data/uci-power-active-2007.csv -o power.tsz
tsz inspect power.tsz
tsz decode power.tsz | head -3
tsz verify data/*.csv
# The suite, then the soak and the size check CI adds.
cargo test
make soak bench-sizes
Rust 1.74 or newer and nothing else for the crate. The bench in bench/ is its own package and pulls zstd, pco and the tsz crate for comparison; fuzzing needs nightly and cargo-fuzz.