mirror of
https://github.com/prometheus/prometheus.git
synced 2025-03-05 20:59:13 -08:00
xor encoding is fast enough for our purposes and provides very good compression. We remove all other ones that partially don't support floats for the sake of simplicity.
341 lines
6.8 KiB
Go
341 lines
6.8 KiB
Go
package chunks
|
||
|
||
import (
|
||
"encoding/binary"
|
||
"math"
|
||
|
||
bits "github.com/dgryski/go-bits"
|
||
)
|
||
|
||
// XORChunk holds XOR encoded sample data.
|
||
type XORChunk struct {
|
||
b *bstream
|
||
num uint16
|
||
sz int
|
||
}
|
||
|
||
// NewXORChunk returns a new chunk with XOR encoding of the given size.
|
||
func NewXORChunk(size int) *XORChunk {
|
||
b := make([]byte, 3, 64)
|
||
b[0] = byte(EncXOR)
|
||
|
||
return &XORChunk{
|
||
b: &bstream{stream: b, count: 0},
|
||
sz: size,
|
||
num: 0,
|
||
}
|
||
}
|
||
|
||
// Bytes returns the underlying byte slice of the chunk.
|
||
func (c *XORChunk) Bytes() []byte {
|
||
b := c.b.bytes()
|
||
// Lazily populate length bytes – probably not necessary to have the
|
||
// cache value in struct.
|
||
binary.LittleEndian.PutUint16(b[1:3], c.num)
|
||
return b
|
||
}
|
||
|
||
// Appender implements the Chunk interface.
|
||
func (c *XORChunk) Appender() (Appender, error) {
|
||
it := c.iterator()
|
||
|
||
// To get an appender we must know the state it would have if we had
|
||
// appended all existing data from scratch.
|
||
// We iterate through the end and populate via the iterator's state.
|
||
for it.Next() {
|
||
}
|
||
if err := it.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return &xorAppender{
|
||
c: c,
|
||
b: c.b,
|
||
t: it.t,
|
||
v: it.val,
|
||
tDelta: it.tDelta,
|
||
leading: it.leading,
|
||
trailing: it.trailing,
|
||
}, nil
|
||
}
|
||
|
||
func (c *XORChunk) iterator() *xorIterator {
|
||
// Should iterators guarantee to act on a copy of the data so it doesn't lock append?
|
||
// When using striped locks to guard access to chunks, probably yes.
|
||
// Could only copy data if the chunk is not completed yet.
|
||
return &xorIterator{
|
||
br: newBReader(c.b.bytes()[3:]),
|
||
numTotal: c.num,
|
||
}
|
||
}
|
||
|
||
// Iterator implements the Chunk interface.
|
||
func (c *XORChunk) Iterator() Iterator {
|
||
return fancyIterator{c.iterator()}
|
||
}
|
||
|
||
type xorAppender struct {
|
||
c *XORChunk
|
||
b *bstream
|
||
|
||
t int64
|
||
v float64
|
||
tDelta uint64
|
||
|
||
leading uint8
|
||
trailing uint8
|
||
}
|
||
|
||
func (a *xorAppender) Append(t int64, v float64) error {
|
||
var tDelta uint64
|
||
|
||
if a.c.num == 0 {
|
||
// TODO: store varint time?
|
||
a.b.writeBits(uint64(t), 64)
|
||
a.b.writeBits(math.Float64bits(v), 64)
|
||
|
||
} else if a.c.num == 1 {
|
||
tDelta = uint64(t - a.t)
|
||
// TODO: use varint or other encoding for first delta?
|
||
a.b.writeBits(tDelta, 64)
|
||
a.writeVDelta(v)
|
||
|
||
} else {
|
||
tDelta = uint64(t - a.t)
|
||
dod := int64(tDelta - a.tDelta)
|
||
|
||
// Gorilla has a max resolution of seconds, Prometheus milliseconds.
|
||
// Thus we use higher value range steps with larger bit size.
|
||
switch {
|
||
case dod == 0:
|
||
a.b.writeBit(zero)
|
||
case -8191 <= dod && dod <= 8192:
|
||
a.b.writeBits(0x02, 2) // '10'
|
||
a.b.writeBits(uint64(dod), 14)
|
||
case -65535 <= dod && dod <= 65536:
|
||
a.b.writeBits(0x06, 3) // '110'
|
||
a.b.writeBits(uint64(dod), 17)
|
||
case -524287 <= dod && dod <= 524288:
|
||
a.b.writeBits(0x0e, 4) // '1110'
|
||
a.b.writeBits(uint64(dod), 20)
|
||
default:
|
||
a.b.writeBits(0x0f, 4) // '1111'
|
||
a.b.writeBits(uint64(dod), 64)
|
||
}
|
||
|
||
a.writeVDelta(v)
|
||
}
|
||
|
||
if len(a.b.bytes()) > a.c.sz {
|
||
return ErrChunkFull
|
||
}
|
||
|
||
a.t = t
|
||
a.v = v
|
||
a.c.num++
|
||
a.tDelta = tDelta
|
||
|
||
return nil
|
||
}
|
||
|
||
func (a *xorAppender) writeVDelta(v float64) {
|
||
vDelta := math.Float64bits(v) ^ math.Float64bits(a.v)
|
||
|
||
if vDelta == 0 {
|
||
a.b.writeBit(zero)
|
||
return
|
||
}
|
||
a.b.writeBit(one)
|
||
|
||
leading := uint8(bits.Clz(vDelta))
|
||
trailing := uint8(bits.Ctz(vDelta))
|
||
|
||
// clamp number of leading zeros to avoid overflow when encoding
|
||
if leading >= 32 {
|
||
leading = 31
|
||
}
|
||
|
||
// TODO(dgryski): check if it's 'cheaper' to reset the leading/trailing bits instead
|
||
if a.leading != ^uint8(0) && leading >= a.leading && trailing >= a.trailing {
|
||
a.b.writeBit(zero)
|
||
a.b.writeBits(vDelta>>a.trailing, 64-int(a.leading)-int(a.trailing))
|
||
} else {
|
||
a.leading, a.trailing = leading, trailing
|
||
|
||
a.b.writeBit(one)
|
||
a.b.writeBits(uint64(leading), 5)
|
||
|
||
// Note that if leading == trailing == 0, then sigbits == 64. But that value doesn't actually fit into the 6 bits we have.
|
||
// Luckily, we never need to encode 0 significant bits, since that would put us in the other case (vdelta == 0).
|
||
// So instead we write out a 0 and adjust it back to 64 on unpacking.
|
||
sigbits := 64 - leading - trailing
|
||
a.b.writeBits(uint64(sigbits), 6)
|
||
a.b.writeBits(vDelta>>trailing, int(sigbits))
|
||
}
|
||
}
|
||
|
||
type xorIterator struct {
|
||
br *bstream
|
||
numTotal uint16
|
||
numRead uint16
|
||
|
||
t int64
|
||
val float64
|
||
|
||
leading uint8
|
||
trailing uint8
|
||
|
||
tDelta uint64
|
||
err error
|
||
}
|
||
|
||
func (it *xorIterator) Values() (int64, float64) {
|
||
return it.t, it.val
|
||
}
|
||
|
||
func (it *xorIterator) Err() error {
|
||
return it.err
|
||
}
|
||
|
||
func (it *xorIterator) Next() bool {
|
||
if it.err != nil || it.numRead == it.numTotal {
|
||
return false
|
||
}
|
||
|
||
if it.numRead == 0 {
|
||
t, err := it.br.readBits(64)
|
||
if err != nil {
|
||
it.err = err
|
||
return false
|
||
}
|
||
v, err := it.br.readBits(64)
|
||
if err != nil {
|
||
it.err = err
|
||
return false
|
||
}
|
||
it.t = int64(t)
|
||
it.val = math.Float64frombits(v)
|
||
|
||
it.numRead++
|
||
return true
|
||
}
|
||
if it.numRead == 1 {
|
||
tDelta, err := it.br.readBits(64)
|
||
if err != nil {
|
||
it.err = err
|
||
return false
|
||
}
|
||
it.tDelta = tDelta
|
||
it.t = it.t + int64(it.tDelta)
|
||
|
||
return it.readValue()
|
||
}
|
||
|
||
var d byte
|
||
// read delta-of-delta
|
||
for i := 0; i < 4; i++ {
|
||
d <<= 1
|
||
bit, err := it.br.readBit()
|
||
if err != nil {
|
||
it.err = err
|
||
return false
|
||
}
|
||
if bit == zero {
|
||
break
|
||
}
|
||
d |= 1
|
||
}
|
||
var sz uint8
|
||
var dod int64
|
||
switch d {
|
||
case 0x00:
|
||
// dod == 0
|
||
case 0x02:
|
||
sz = 14
|
||
case 0x06:
|
||
sz = 17
|
||
case 0x0e:
|
||
sz = 20
|
||
case 0x0f:
|
||
bits, err := it.br.readBits(64)
|
||
if err != nil {
|
||
it.err = err
|
||
return false
|
||
}
|
||
|
||
dod = int64(bits)
|
||
}
|
||
|
||
if sz != 0 {
|
||
bits, err := it.br.readBits(int(sz))
|
||
if err != nil {
|
||
it.err = err
|
||
return false
|
||
}
|
||
if bits > (1 << (sz - 1)) {
|
||
// or something
|
||
bits = bits - (1 << sz)
|
||
}
|
||
dod = int64(bits)
|
||
}
|
||
|
||
it.tDelta = uint64(int64(it.tDelta) + dod)
|
||
it.t = it.t + int64(it.tDelta)
|
||
|
||
return it.readValue()
|
||
}
|
||
|
||
func (it *xorIterator) readValue() bool {
|
||
bit, err := it.br.readBit()
|
||
if err != nil {
|
||
it.err = err
|
||
return false
|
||
}
|
||
|
||
if bit == zero {
|
||
// it.val = it.val
|
||
} else {
|
||
bit, err := it.br.readBit()
|
||
if err != nil {
|
||
it.err = err
|
||
return false
|
||
}
|
||
if bit == zero {
|
||
// reuse leading/trailing zero bits
|
||
// it.leading, it.trailing = it.leading, it.trailing
|
||
} else {
|
||
bits, err := it.br.readBits(5)
|
||
if err != nil {
|
||
it.err = err
|
||
return false
|
||
}
|
||
it.leading = uint8(bits)
|
||
|
||
bits, err = it.br.readBits(6)
|
||
if err != nil {
|
||
it.err = err
|
||
return false
|
||
}
|
||
mbits := uint8(bits)
|
||
// 0 significant bits here means we overflowed and we actually need 64; see comment in encoder
|
||
if mbits == 0 {
|
||
mbits = 64
|
||
}
|
||
it.trailing = 64 - it.leading - mbits
|
||
}
|
||
|
||
mbits := int(64 - it.leading - it.trailing)
|
||
bits, err := it.br.readBits(mbits)
|
||
if err != nil {
|
||
it.err = err
|
||
return false
|
||
}
|
||
vbits := math.Float64bits(it.val)
|
||
vbits ^= (bits << it.trailing)
|
||
it.val = math.Float64frombits(vbits)
|
||
}
|
||
|
||
it.numRead++
|
||
return true
|
||
}
|