mirror of https://github.com/prometheus/prometheus
Make floats exact again.
This should do the right thing for the old delta chunks, too.pull/579/head
parent
a8d4f8af9a
commit
23ba8a5516
|
@ -124,10 +124,11 @@ func (c deltaEncodedChunk) add(s *metric.SamplePair) []chunk {
|
||||||
return []chunk{&c, overflowChunks[0]}
|
return []chunk{&c, overflowChunks[0]}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
baseValue := c.baseValue()
|
||||||
// TODO(beorn7): Once https://github.com/prometheus/prometheus/issues/481 is
|
// TODO(beorn7): Once https://github.com/prometheus/prometheus/issues/481 is
|
||||||
// fixed, we should panic here if dt is negative.
|
// fixed, we should panic here if dt is negative.
|
||||||
dt := s.Timestamp - c.baseTime()
|
dt := s.Timestamp - c.baseTime()
|
||||||
dv := s.Value - c.baseValue()
|
dv := s.Value - baseValue
|
||||||
tb := c.timeBytes()
|
tb := c.timeBytes()
|
||||||
vb := c.valueBytes()
|
vb := c.valueBytes()
|
||||||
|
|
||||||
|
@ -139,18 +140,23 @@ func (c deltaEncodedChunk) add(s *metric.SamplePair) []chunk {
|
||||||
return transcodeAndAdd(newDeltaEncodedChunk(tb, d4, false, cap(c)), &c, s)
|
return transcodeAndAdd(newDeltaEncodedChunk(tb, d4, false, cap(c)), &c, s)
|
||||||
}
|
}
|
||||||
// float32->float64.
|
// float32->float64.
|
||||||
if !c.isInt() && vb == d4 && !isFloat32(dv) {
|
if !c.isInt() && vb == d4 && baseValue+clientmodel.SampleValue(float32(dv)) != s.Value {
|
||||||
return transcodeAndAdd(newDeltaEncodedChunk(tb, d8, false, cap(c)), &c, s)
|
return transcodeAndAdd(newDeltaEncodedChunk(tb, d8, false, cap(c)), &c, s)
|
||||||
}
|
}
|
||||||
if tb < d8 || vb < d8 {
|
|
||||||
// Maybe more bytes per sample.
|
var ntb, nvb deltaBytes
|
||||||
ntb := bytesNeededForUnsignedTimestampDelta(dt)
|
if tb < d8 {
|
||||||
nvb := bytesNeededForSampleValueDelta(dv, c.isInt())
|
// Maybe more bytes for timestamp.
|
||||||
if ntb > tb || nvb > vb {
|
ntb = bytesNeededForUnsignedTimestampDelta(dt)
|
||||||
ntb = max(ntb, tb)
|
}
|
||||||
nvb = max(nvb, vb)
|
if c.isInt() && vb < d8 {
|
||||||
return transcodeAndAdd(newDeltaEncodedChunk(ntb, nvb, c.isInt(), cap(c)), &c, s)
|
// Maybe more bytes for sample value.
|
||||||
}
|
nvb = bytesNeededForIntegerSampleValueDelta(dv)
|
||||||
|
}
|
||||||
|
if ntb > tb || nvb > vb {
|
||||||
|
ntb = max(ntb, tb)
|
||||||
|
nvb = max(nvb, vb)
|
||||||
|
return transcodeAndAdd(newDeltaEncodedChunk(ntb, nvb, c.isInt(), cap(c)), &c, s)
|
||||||
}
|
}
|
||||||
offset := len(c)
|
offset := len(c)
|
||||||
c = c[:offset+sampleSize]
|
c = c[:offset+sampleSize]
|
||||||
|
|
|
@ -55,25 +55,19 @@ func bytesNeededForSignedTimestampDelta(deltaT clientmodel.Timestamp) deltaBytes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func bytesNeededForSampleValueDelta(deltaV clientmodel.SampleValue, isInt bool) deltaBytes {
|
func bytesNeededForIntegerSampleValueDelta(deltaV clientmodel.SampleValue) deltaBytes {
|
||||||
if isInt {
|
switch {
|
||||||
switch {
|
case deltaV < math.MinInt32 || deltaV > math.MaxInt32:
|
||||||
case deltaV < math.MinInt32 || deltaV > math.MaxInt32:
|
|
||||||
return d8
|
|
||||||
case deltaV < math.MinInt16 || deltaV > math.MaxInt16:
|
|
||||||
return d4
|
|
||||||
case deltaV < math.MinInt8 || deltaV > math.MaxInt8:
|
|
||||||
return d2
|
|
||||||
case deltaV != 0:
|
|
||||||
return d1
|
|
||||||
default:
|
|
||||||
return d0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if clientmodel.SampleValue(float32(deltaV)) != deltaV {
|
|
||||||
return d8
|
return d8
|
||||||
|
case deltaV < math.MinInt16 || deltaV > math.MaxInt16:
|
||||||
|
return d4
|
||||||
|
case deltaV < math.MinInt8 || deltaV > math.MaxInt8:
|
||||||
|
return d2
|
||||||
|
case deltaV != 0:
|
||||||
|
return d1
|
||||||
|
default:
|
||||||
|
return d0
|
||||||
}
|
}
|
||||||
return d4
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func max(a, b deltaBytes) deltaBytes {
|
func max(a, b deltaBytes) deltaBytes {
|
||||||
|
@ -88,8 +82,3 @@ func isInt64(v clientmodel.SampleValue) bool {
|
||||||
// Note: Using math.Modf is slower than the conversion approach below.
|
// Note: Using math.Modf is slower than the conversion approach below.
|
||||||
return clientmodel.SampleValue(int64(v)) == v
|
return clientmodel.SampleValue(int64(v)) == v
|
||||||
}
|
}
|
||||||
|
|
||||||
// isFloat32 returns true if v can be represented as an float32.
|
|
||||||
func isFloat32(v clientmodel.SampleValue) bool {
|
|
||||||
return clientmodel.SampleValue(float32(v)) == v
|
|
||||||
}
|
|
||||||
|
|
|
@ -171,40 +171,49 @@ func (c doubleDeltaEncodedChunk) add(s *metric.SamplePair) []chunk {
|
||||||
return []chunk{&c, overflowChunks[0]}
|
return []chunk{&c, overflowChunks[0]}
|
||||||
}
|
}
|
||||||
|
|
||||||
dt := s.Timestamp - c.baseTime() - clientmodel.Timestamp(c.len())*c.baseTimeDelta()
|
projectedTime := c.baseTime() + clientmodel.Timestamp(c.len())*c.baseTimeDelta()
|
||||||
dv := s.Value - c.baseValue() - clientmodel.SampleValue(c.len())*c.baseValueDelta()
|
ddt := s.Timestamp - projectedTime
|
||||||
|
|
||||||
|
projectedValue := c.baseValue() + clientmodel.SampleValue(c.len())*c.baseValueDelta()
|
||||||
|
ddv := s.Value - projectedValue
|
||||||
|
|
||||||
// If the new sample is incompatible with the current encoding, reencode the
|
// If the new sample is incompatible with the current encoding, reencode the
|
||||||
// existing chunk data into new chunk(s).
|
// existing chunk data into new chunk(s).
|
||||||
//
|
//
|
||||||
// int->float.
|
// int->float.
|
||||||
if c.isInt() && !isInt64(dv) {
|
if c.isInt() && !isInt64(ddv) {
|
||||||
return transcodeAndAdd(newDoubleDeltaEncodedChunk(tb, d4, false, cap(c)), &c, s)
|
return transcodeAndAdd(newDoubleDeltaEncodedChunk(tb, d4, false, cap(c)), &c, s)
|
||||||
}
|
}
|
||||||
// float32->float64.
|
// float32->float64.
|
||||||
if !c.isInt() && vb == d4 && !isFloat32(dv) {
|
if !c.isInt() && vb == d4 && projectedValue+clientmodel.SampleValue(float32(ddv)) != s.Value {
|
||||||
return transcodeAndAdd(newDoubleDeltaEncodedChunk(tb, d8, false, cap(c)), &c, s)
|
return transcodeAndAdd(newDoubleDeltaEncodedChunk(tb, d8, false, cap(c)), &c, s)
|
||||||
}
|
}
|
||||||
if tb < d8 || vb < d8 {
|
|
||||||
// Maybe more bytes per sample.
|
var ntb, nvb deltaBytes
|
||||||
ntb := bytesNeededForSignedTimestampDelta(dt)
|
if tb < d8 {
|
||||||
nvb := bytesNeededForSampleValueDelta(dv, c.isInt())
|
// Maybe more bytes for timestamp.
|
||||||
if ntb > tb || nvb > vb {
|
ntb = bytesNeededForSignedTimestampDelta(ddt)
|
||||||
ntb = max(ntb, tb)
|
|
||||||
nvb = max(nvb, vb)
|
|
||||||
return transcodeAndAdd(newDoubleDeltaEncodedChunk(ntb, nvb, c.isInt(), cap(c)), &c, s)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if c.isInt() && vb < d8 {
|
||||||
|
// Maybe more bytes for sample value.
|
||||||
|
nvb = bytesNeededForIntegerSampleValueDelta(ddv)
|
||||||
|
}
|
||||||
|
if ntb > tb || nvb > vb {
|
||||||
|
ntb = max(ntb, tb)
|
||||||
|
nvb = max(nvb, vb)
|
||||||
|
return transcodeAndAdd(newDeltaEncodedChunk(ntb, nvb, c.isInt(), cap(c)), &c, s)
|
||||||
|
}
|
||||||
|
|
||||||
offset := len(c)
|
offset := len(c)
|
||||||
c = c[:offset+sampleSize]
|
c = c[:offset+sampleSize]
|
||||||
|
|
||||||
switch tb {
|
switch tb {
|
||||||
case d1:
|
case d1:
|
||||||
c[offset] = byte(dt)
|
c[offset] = byte(ddt)
|
||||||
case d2:
|
case d2:
|
||||||
binary.LittleEndian.PutUint16(c[offset:], uint16(dt))
|
binary.LittleEndian.PutUint16(c[offset:], uint16(ddt))
|
||||||
case d4:
|
case d4:
|
||||||
binary.LittleEndian.PutUint32(c[offset:], uint32(dt))
|
binary.LittleEndian.PutUint32(c[offset:], uint32(ddt))
|
||||||
case d8:
|
case d8:
|
||||||
// Store the absolute value (no delta) in case of d8.
|
// Store the absolute value (no delta) in case of d8.
|
||||||
binary.LittleEndian.PutUint64(c[offset:], uint64(s.Timestamp))
|
binary.LittleEndian.PutUint64(c[offset:], uint64(s.Timestamp))
|
||||||
|
@ -219,11 +228,11 @@ func (c doubleDeltaEncodedChunk) add(s *metric.SamplePair) []chunk {
|
||||||
case d0:
|
case d0:
|
||||||
// No-op. Constant delta is stored as base value.
|
// No-op. Constant delta is stored as base value.
|
||||||
case d1:
|
case d1:
|
||||||
c[offset] = byte(dv)
|
c[offset] = byte(ddv)
|
||||||
case d2:
|
case d2:
|
||||||
binary.LittleEndian.PutUint16(c[offset:], uint16(dv))
|
binary.LittleEndian.PutUint16(c[offset:], uint16(ddv))
|
||||||
case d4:
|
case d4:
|
||||||
binary.LittleEndian.PutUint32(c[offset:], uint32(dv))
|
binary.LittleEndian.PutUint32(c[offset:], uint32(ddv))
|
||||||
// d8 must not happen. Those samples are encoded as float64.
|
// d8 must not happen. Those samples are encoded as float64.
|
||||||
default:
|
default:
|
||||||
panic("invalid number of bytes for integer delta")
|
panic("invalid number of bytes for integer delta")
|
||||||
|
@ -231,7 +240,7 @@ func (c doubleDeltaEncodedChunk) add(s *metric.SamplePair) []chunk {
|
||||||
} else {
|
} else {
|
||||||
switch vb {
|
switch vb {
|
||||||
case d4:
|
case d4:
|
||||||
binary.LittleEndian.PutUint32(c[offset:], math.Float32bits(float32(dv)))
|
binary.LittleEndian.PutUint32(c[offset:], math.Float32bits(float32(ddv)))
|
||||||
case d8:
|
case d8:
|
||||||
// Store the absolute value (no delta) in case of d8.
|
// Store the absolute value (no delta) in case of d8.
|
||||||
binary.LittleEndian.PutUint64(c[offset:], math.Float64bits(float64(s.Value)))
|
binary.LittleEndian.PutUint64(c[offset:], math.Float64bits(float64(s.Value)))
|
||||||
|
|
|
@ -15,7 +15,6 @@ package local
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -30,14 +29,6 @@ import (
|
||||||
"github.com/prometheus/prometheus/utility/test"
|
"github.com/prometheus/prometheus/utility/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
epsilon = 0.000001 // Relative error allowed for sample values.
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
minNormal = math.Float64frombits(0x0010000000000000) // The smallest positive normal value of type float64.
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGetFingerprintsForLabelMatchers(t *testing.T) {
|
func TestGetFingerprintsForLabelMatchers(t *testing.T) {
|
||||||
storage, closer := NewTestStorage(t, 1)
|
storage, closer := NewTestStorage(t, 1)
|
||||||
defer closer.Close()
|
defer closer.Close()
|
||||||
|
@ -221,7 +212,7 @@ func testChunk(t *testing.T, chunkType byte) {
|
||||||
if samples[i].Timestamp != v.Timestamp {
|
if samples[i].Timestamp != v.Timestamp {
|
||||||
t.Errorf("%d. Got %v; want %v", i, v.Timestamp, samples[i].Timestamp)
|
t.Errorf("%d. Got %v; want %v", i, v.Timestamp, samples[i].Timestamp)
|
||||||
}
|
}
|
||||||
if !almostEqual(samples[i].Value, v.Value) {
|
if samples[i].Value != v.Value {
|
||||||
t.Errorf("%d. Got %v; want %v", i, v.Value, samples[i].Value)
|
t.Errorf("%d. Got %v; want %v", i, v.Value, samples[i].Value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -655,7 +646,7 @@ func TestFuzzChunkType1(t *testing.T) {
|
||||||
//
|
//
|
||||||
// go test -race -cpu 8 -test=short -bench BenchmarkFuzzChunkType
|
// go test -race -cpu 8 -test=short -bench BenchmarkFuzzChunkType
|
||||||
func benchmarkFuzz(b *testing.B, chunkType byte) {
|
func benchmarkFuzz(b *testing.B, chunkType byte) {
|
||||||
const samplesPerRun = 20000
|
const samplesPerRun = 100000
|
||||||
rand.Seed(42)
|
rand.Seed(42)
|
||||||
directory := test.NewTemporaryDirectory("test_storage", b)
|
directory := test.NewTemporaryDirectory("test_storage", b)
|
||||||
defer directory.Close()
|
defer directory.Close()
|
||||||
|
@ -837,7 +828,7 @@ func verifyStorage(t testing.TB, s Storage, samples clientmodel.Samples, maxAge
|
||||||
}
|
}
|
||||||
want := sample.Value
|
want := sample.Value
|
||||||
got := found[0].Value
|
got := found[0].Value
|
||||||
if !almostEqual(want, got) || sample.Timestamp != found[0].Timestamp {
|
if want != got || sample.Timestamp != found[0].Timestamp {
|
||||||
t.Errorf(
|
t.Errorf(
|
||||||
"Value (or timestamp) mismatch, want %f (at time %v), got %f (at time %v).",
|
"Value (or timestamp) mismatch, want %f (at time %v), got %f (at time %v).",
|
||||||
want, sample.Timestamp, got, found[0].Timestamp,
|
want, sample.Timestamp, got, found[0].Timestamp,
|
||||||
|
@ -938,15 +929,3 @@ func TestChunkMaps(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func almostEqual(a, b clientmodel.SampleValue) bool {
|
|
||||||
// Cf. http://floating-point-gui.de/errors/comparison/
|
|
||||||
if a == b {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
diff := math.Abs(float64(a - b))
|
|
||||||
if a == 0 || b == 0 || diff < minNormal {
|
|
||||||
return diff < epsilon*minNormal
|
|
||||||
}
|
|
||||||
return diff/(math.Abs(float64(a))+math.Abs(float64(b))) < epsilon
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue