Skip to content

Commit 64aa547

Browse files
committedFeb 18, 2020
[doc] Add survey for prometheus encode
- [log] Add survey on primitive types, covered endianess
1 parent a6693fb commit 64aa547

File tree

9 files changed

+222
-6
lines changed

9 files changed

+222
-6
lines changed
 

‎.gitignore

+2-4
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,8 @@
1010
# Output of the go coverage tool, specifically when used with LiteIDE
1111
*.out
1212

13-
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
14-
.glide/
15-
1613
.idea
1714
.vscode
1815

19-
vendor
16+
# C code used in playground
17+
a.out

‎doc/compression/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ Compression for both time series data and the meta data in index
44

55
## TODO
66

7+
- [ ] primitive types, how is int64, uint64, float64 stored and what happens when there is cast/conversion
78
- [ ] gorilla
89
- [ ] https://roaringbitmap.org/ I think used by both influxdb and m3 (and maybe more)

‎doc/database/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ API
2525
Internal
2626

2727
- **model.md** General data model, what is a time series for this TSDB (yeah, this definition varies).
28+
- **compression.md** Compression related algorithm or code.
2829
- **query-execution.md** Query execution and optimization, especially for those with query language and distributed ones.
2930
- **storage-engine.md** Only applies to TSDB w/ their own storage format, i.e. write opaque blob to local fs or object store.
3031
- **schema.md** Only applies to TSDB w/ underlying database i.e. Cassandra, ElasticSearch
+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# Prometheus Compression
2+
3+
Compression for data is in [tsdb/chunkenc](https://github.com/prometheus/prometheus/tree/master/tsdb/chunkenc).
4+
5+
## Encode
6+
7+
There is a `Appender` interface and two implementations, the second one is just a thread safe wrapper.
8+
The main logic is in [xorAppender.Append](https://github.com/prometheus/prometheus/blob/0703dae7cc4fcb1e051ab5fec89c47530e78c75a/tsdb/chunkenc/xor.go#L149)
9+
which contains the double delta logic for timestamp and deal with float64 in [xorAppender.writeVDelta](https://github.com/prometheus/prometheus/blob/0703dae7cc4fcb1e051ab5fec89c47530e78c75a/tsdb/chunkenc/xor.go#L206)
10+
11+
```go
12+
// tsdb/chunenc/xor.go
13+
14+
type xorAppender struct {
15+
b *bstream
16+
17+
t int64
18+
v float64
19+
tDelta uint64
20+
21+
leading uint8
22+
trailing uint8
23+
}
24+
25+
func (a *xorAppender) Append(t int64, v float64) {
26+
var tDelta uint64
27+
num := binary.BigEndian.Uint16(a.b.bytes())
28+
29+
if num == 0 {
30+
buf := make([]byte, binary.MaxVarintLen64)
31+
for _, b := range buf[:binary.PutVarint(buf, t)] {
32+
a.b.writeByte(b)
33+
}
34+
a.b.writeBits(math.Float64bits(v), 64)
35+
36+
} else if num == 1 {
37+
tDelta = uint64(t - a.t)
38+
39+
buf := make([]byte, binary.MaxVarintLen64)
40+
for _, b := range buf[:binary.PutUvarint(buf, tDelta)] {
41+
a.b.writeByte(b)
42+
}
43+
44+
a.writeVDelta(v)
45+
46+
} else {
47+
tDelta = uint64(t - a.t)
48+
dod := int64(tDelta - a.tDelta)
49+
50+
// Gorilla has a max resolution of seconds, Prometheus milliseconds.
51+
// Thus we use higher value range steps with larger bit size.
52+
switch {
53+
case dod == 0:
54+
a.b.writeBit(zero)
55+
case bitRange(dod, 14):
56+
a.b.writeBits(0x02, 2) // '10'
57+
a.b.writeBits(uint64(dod), 14)
58+
case bitRange(dod, 17):
59+
a.b.writeBits(0x06, 3) // '110'
60+
a.b.writeBits(uint64(dod), 17)
61+
case bitRange(dod, 20):
62+
a.b.writeBits(0x0e, 4) // '1110'
63+
a.b.writeBits(uint64(dod), 20)
64+
default:
65+
a.b.writeBits(0x0f, 4) // '1111'
66+
a.b.writeBits(uint64(dod), 64)
67+
}
68+
69+
a.writeVDelta(v)
70+
}
71+
72+
a.t = t
73+
a.v = v
74+
binary.BigEndian.PutUint16(a.b.bytes(), num+1)
75+
a.tDelta = tDelta
76+
}
77+
78+
func (a *xorAppender) writeVDelta(v float64) {
79+
vDelta := math.Float64bits(v) ^ math.Float64bits(a.v)
80+
81+
if vDelta == 0 {
82+
a.b.writeBit(zero)
83+
return
84+
}
85+
a.b.writeBit(one)
86+
87+
leading := uint8(bits.LeadingZeros64(vDelta))
88+
trailing := uint8(bits.TrailingZeros64(vDelta))
89+
90+
// Clamp number of leading zeros to avoid overflow when encoding.
91+
if leading >= 32 {
92+
leading = 31
93+
}
94+
95+
if a.leading != 0xff && leading >= a.leading && trailing >= a.trailing {
96+
a.b.writeBit(zero)
97+
a.b.writeBits(vDelta>>a.trailing, 64-int(a.leading)-int(a.trailing))
98+
} else {
99+
a.leading, a.trailing = leading, trailing
100+
101+
a.b.writeBit(one)
102+
a.b.writeBits(uint64(leading), 5)
103+
104+
// Note that if leading == trailing == 0, then sigbits == 64. But that value doesn't actually fit into the 6 bits we have.
105+
// Luckily, we never need to encode 0 significant bits, since that would put us in the other case (vdelta == 0).
106+
// So instead we write out a 0 and adjust it back to 64 on unpacking.
107+
sigbits := 64 - leading - trailing
108+
a.b.writeBits(uint64(sigbits), 6)
109+
a.b.writeBits(vDelta>>trailing, int(sigbits))
110+
}
111+
}
112+
```

‎doc/log/2020-02/2020-02-03.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@ Should be able to have
1313

1414
## TODO
1515

16-
- [ ] tsz compression, at least a double delta example
16+
- [x] tsz compression, at least a double delta example

‎doc/log/2020-02/2020-02-17.md

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# 2020-02-17
2+
3+
Didn't spent time on libtsdb for about two weeks (due to playing WOT ...).
4+
Double delta part on timestamp is finished, most time is spent on writing a bit stream writer/reader.
5+
6+
go-tsz would change the underlying byte slice so prometheus modified it because they are using mmap.
7+
Though their iterator still need to reset to be able to use again.
8+
9+
## TODO
10+
11+
- [ ] survey: how is primitive type stored and what happens during cast
12+
- [ ] big endian, small endian
13+
- network order is big endian
14+
- [ ] CPU, disk? intel, amd?
15+
- [ ] int, unsigned int
16+
- the unsigned int to int conversion (why it worked in code)
17+
- [ ] `2s complement` as mentioned by Haiyu
18+
- [ ] shift behavior on signed and unsigned
19+
- [ ] float64
20+
- [ ] how is it saved
21+
- [ ] what happens when casting from float64 to uint64
22+
- [ ] prometheus
23+
- [ ] what would happen to its xorAppender when appending time out of order
24+
- [ ] finalize the stream, it seems it is writing number of samples at the end of stream ...
25+
26+
## Endianness
27+
28+
https://en.wikipedia.org/wiki/Endianness
29+
30+
- big endian, e.g. 1024 is `2^10`, big endian `100` `0000_0000`, small endian `0000_0000` `100`
31+
- network use big endian
32+
33+
> A big-endian ordering places the most significant byte first and the least significant byte last,
34+
> while a little-endian ordering does the opposite
35+
36+
> big-endianness is the dominant ordering in networking protocols
37+
> little-endianness is the dominant ordering for processor architectures and their associated memory
38+
> File formats can use either ordering
39+
40+
> programming languages use big-endian digit ordering for numeric literals
41+
> as well as big-endian language (“left” and “right”) for bit-shift operations,
42+
> regardless of the endianness of the target architecture

‎playground/gorilla/gorilla_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ func TestDoubleDelta(t *testing.T) {
299299
log.Printf("%v", r.buf)
300300
assert.Nil(t, err)
301301
log.Print(v)
302-
assert.Equal(t, -2, uint2int(v, 7))
302+
assert.Equal(t, int64(-2), uint2int(v, 7))
303303
}
304304

305305
// TODO: why this work ...

‎playground/primitive/primitive.c

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#include<stdio.h>
2+
3+
int main() {
4+
int a = 1024;
5+
printf("%ld %d\n", sizeof(a), a);
6+
// cast directly to see endianness
7+
unsigned char* bytes = (unsigned char*) &a;
8+
for (int i = 0; i < 4; i++) {
9+
printf("%u ", bytes[i]);
10+
}
11+
printf("\n");
12+
}
13+
14+
// 4 1024
15+
// 0 4 0 0
+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package primitive_test
2+
3+
import (
4+
"encoding/binary"
5+
"reflect"
6+
"testing"
7+
"unsafe"
8+
)
9+
10+
// test primitive types
11+
12+
func TestEndianness(t *testing.T) {
13+
v := uint64(1024)
14+
var buf [8]byte
15+
// The implementation simply do right shift
16+
binary.BigEndian.PutUint64(buf[:], v)
17+
// [0 0 0 0 0 0 4 0], which is
18+
// b[0] = byte(v >> 56)
19+
// b[1] = byte(v >> 48)
20+
// ...
21+
// b[6] = byte(v >> 8)
22+
// b[7] = byte(v)
23+
t.Logf("%v", buf)
24+
25+
// The implementation is also doing right shift ... just different order
26+
// _ = b[7] // early bounds check to guarantee safety of writes below
27+
// b[0] = byte(v)
28+
// b[1] = byte(v >> 8)
29+
// b[2] = byte(v >> 16)
30+
// ...
31+
// b[7] = byte(v >> 56)
32+
binary.LittleEndian.PutUint64(buf[:], v)
33+
t.Logf("%v", buf)
34+
35+
// [0 4 0 0 0 0 0 0] it's little endian when using unsafe, ok ...
36+
t.Logf("%v", unsafeInt2Bytes(v))
37+
}
38+
39+
// https://stackoverflow.com/a/17539687
40+
func unsafeInt2Bytes(v uint64) []byte {
41+
hdr := reflect.SliceHeader{
42+
Data: uintptr(unsafe.Pointer(&v)),
43+
Len: 8,
44+
Cap: 8,
45+
}
46+
return *(*[]byte)(unsafe.Pointer(&hdr))
47+
}

0 commit comments

Comments
 (0)
Please sign in to comment.