Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 9 additions & 13 deletions prometheus/counter.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,19 +96,15 @@ func NewCounterVec(opts CounterOpts, labelNames []string) *CounterVec {
opts.ConstLabels,
)
return &CounterVec{
MetricVec: MetricVec{
children: map[uint64]Metric{},
desc: desc,
newMetric: func(lvs ...string) Metric {
result := &counter{value: value{
desc: desc,
valType: CounterValue,
labelPairs: makeLabelPairs(desc, lvs),
}}
result.init(result) // Init self-collection.
return result
},
},
MetricVec: newMetricVec(desc, func(lvs ...string) Metric {
result := &counter{value: value{
desc: desc,
valType: CounterValue,
labelPairs: makeLabelPairs(desc, lvs),
}}
result.init(result) // Init self-collection.
return result
}),
}
}

Expand Down
10 changes: 3 additions & 7 deletions prometheus/gauge.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,9 @@ func NewGaugeVec(opts GaugeOpts, labelNames []string) *GaugeVec {
opts.ConstLabels,
)
return &GaugeVec{
MetricVec: MetricVec{
children: map[uint64]Metric{},
desc: desc,
newMetric: func(lvs ...string) Metric {
return newValue(desc, GaugeValue, 0, lvs...)
},
},
MetricVec: newMetricVec(desc, func(lvs ...string) Metric {
return newValue(desc, GaugeValue, 0, lvs...)
}),
}
}

Expand Down
10 changes: 3 additions & 7 deletions prometheus/histogram.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,13 +301,9 @@ func NewHistogramVec(opts HistogramOpts, labelNames []string) *HistogramVec {
opts.ConstLabels,
)
return &HistogramVec{
MetricVec: MetricVec{
children: map[uint64]Metric{},
desc: desc,
newMetric: func(lvs ...string) Metric {
return newHistogram(desc, opts, lvs...)
},
},
MetricVec: newMetricVec(desc, func(lvs ...string) Metric {
return newHistogram(desc, opts, lvs...)
}),
}
}

Expand Down
10 changes: 3 additions & 7 deletions prometheus/summary.go
Original file line number Diff line number Diff line change
Expand Up @@ -404,13 +404,9 @@ func NewSummaryVec(opts SummaryOpts, labelNames []string) *SummaryVec {
opts.ConstLabels,
)
return &SummaryVec{
MetricVec: MetricVec{
children: map[uint64]Metric{},
desc: desc,
newMetric: func(lvs ...string) Metric {
return newSummary(desc, opts, lvs...)
},
},
MetricVec: newMetricVec(desc, func(lvs ...string) Metric {
return newSummary(desc, opts, lvs...)
}),
}
}

Expand Down
10 changes: 3 additions & 7 deletions prometheus/untyped.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,9 @@ func NewUntypedVec(opts UntypedOpts, labelNames []string) *UntypedVec {
opts.ConstLabels,
)
return &UntypedVec{
MetricVec: MetricVec{
children: map[uint64]Metric{},
desc: desc,
newMetric: func(lvs ...string) Metric {
return newValue(desc, UntypedValue, 0, lvs...)
},
},
MetricVec: newMetricVec(desc, func(lvs ...string) Metric {
return newValue(desc, UntypedValue, 0, lvs...)
}),
}
}

Expand Down
215 changes: 171 additions & 44 deletions prometheus/vec.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ package prometheus
import (
"fmt"
"sync"

"github.com/prometheus/common/model"
)

// MetricVec is a Collector to bundle metrics of the same name that
Expand All @@ -25,10 +27,31 @@ import (
// provided in this package.
type MetricVec struct {
mtx sync.RWMutex // Protects the children.
children map[uint64]Metric
children map[uint64][]metricWithLabelValues
desc *Desc

newMetric func(labelValues ...string) Metric
newMetric func(labelValues ...string) Metric
hashAdd func(h uint64, s string) uint64 // replace hash function for testing collision handling
hashAddByte func(h uint64, b byte) uint64
}

// newMetricVec returns an initialized MetricVec. The concrete value is
// returned for embedding into another struct.
func newMetricVec(desc *Desc, newMetric func(lvs ...string) Metric) MetricVec {
return MetricVec{
children: map[uint64][]metricWithLabelValues{},
desc: desc,
newMetric: newMetric,
hashAdd: hashAdd,
hashAddByte: hashAddByte,
}
}

// metricWithLabelValues provides the metric and its label values for
// disambiguation on hash collision.
type metricWithLabelValues struct {
values []string
metric Metric
}

// Describe implements Collector. The length of the returned slice
Expand All @@ -42,8 +65,10 @@ func (m *MetricVec) Collect(ch chan<- Metric) {
m.mtx.RLock()
defer m.mtx.RUnlock()

for _, metric := range m.children {
ch <- metric
for _, metrics := range m.children {
for _, metric := range metrics {
ch <- metric.metric
}
}
}

Expand Down Expand Up @@ -77,16 +102,7 @@ func (m *MetricVec) GetMetricWithLabelValues(lvs ...string) (Metric, error) {
return nil, err
}

m.mtx.RLock()
metric, ok := m.children[h]
m.mtx.RUnlock()
if ok {
return metric, nil
}

m.mtx.Lock()
defer m.mtx.Unlock()
return m.getOrCreateMetric(h, lvs...), nil
return m.getOrCreateMetric(h, lvs), nil
}

// GetMetricWith returns the Metric for the given Labels map (the label names
Expand All @@ -107,20 +123,7 @@ func (m *MetricVec) GetMetricWith(labels Labels) (Metric, error) {
return nil, err
}

m.mtx.RLock()
metric, ok := m.children[h]
m.mtx.RUnlock()
if ok {
return metric, nil
}

lvs := make([]string, len(labels))
for i, label := range m.desc.variableLabels {
lvs[i] = labels[label]
}
m.mtx.Lock()
defer m.mtx.Unlock()
return m.getOrCreateMetric(h, lvs...), nil
return m.getOrCreateMetric(h, labels), nil
}

// WithLabelValues works as GetMetricWithLabelValues, but panics if an error
Expand Down Expand Up @@ -168,11 +171,7 @@ func (m *MetricVec) DeleteLabelValues(lvs ...string) bool {
if err != nil {
return false
}
if _, ok := m.children[h]; !ok {
return false
}
delete(m.children, h)
return true
return m.deleteByHash(h, lvs)
}

// Delete deletes the metric where the variable labels are the same as those
Expand All @@ -193,10 +192,31 @@ func (m *MetricVec) Delete(labels Labels) bool {
if err != nil {
return false
}
if _, ok := m.children[h]; !ok {

return m.deleteByHash(h, labels)
}

// deleteByHash removes the metric from the hash bucket h. If there are
// multiple matches in the bucket, use lvs to select a metric and remove only
// that metric.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doc comment is probably a good place to document the requirements for interface{} ("... is of type []string or Labels").

//
// lvs MUST be of type Labels or []string or this method will panic.
func (m *MetricVec) deleteByHash(h uint64, values interface{}) bool {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not really a fan of interface type arguments. We're losing the advantages of the type system. Is this really necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To maintain correctness and zero-alloc behavior without repeating code, it is necessary. Any errors are caught during test time.

What is the issue? This is private code. Are you just afraid?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could, of course, repeat the code. Go isn't really afraid of that. But in this case, it would be a lot of code. I'm fine with the interface usage as it avoids the allocation in L117 of the old code.

It might be good, though, to document the requirements for values at the top of the call stack (even if it is an unexported method). I'll leave a comment where I think it should be documented.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right @stevvooe, I couldn't come up with a typed implementation either without causing more allocations. We'll need to wait until golang's escape analysis becomes better.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@grobie @beorn7 Just made a polymorphic implementation and confirmed we get allocations.

Up to you, but it might be better to allow allocations and wait for better escape analysis. It would avoid the needless complexity in this code. A simple string slice header allocation on each lookup would let us delete a massive swath of code.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we're more interested in as little as possible overhead of the client than a nice and short code base. @beorn7 is the gatekeeper of this repository though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, avoiding allocations in this code path is essential. It is very likely to be called very frequently by users.

Current code is the best solution, I guess.

metrics, ok := m.children[h]
if !ok {
return false
}
delete(m.children, h)

i := m.findMetric(metrics, values)
if i >= len(metrics) {
return false
}

if len(metrics) > 1 {
m.children[h] = append(metrics[:i], metrics[i+1:]...)
} else {
delete(m.children, h)
}
return true
}

Expand All @@ -216,7 +236,8 @@ func (m *MetricVec) hashLabelValues(vals []string) (uint64, error) {
}
h := hashNew()
for _, val := range vals {
h = hashAdd(h, val)
h = m.hashAdd(h, val)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should actually be followed by hashAddByte(h, separatorByte) (this is a bug in the original code already, hash collisions could be created with label values like {"foo", "bar"} and {"foob", "ar"}).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch.

I'll use the 0x00 to keep sort order of tuple in case we decide to optimize this differently.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

separatorByte (= 0xFF) has the advantage that it is not a valid byte value anywhere in a UTF-8 string, while 0x00 is (I believe). It is also consistently used in other hashing in Prometheus code.

Since the hashing is completely internal here, it doesn't really matter. Even if somebody constructed a collision, it doesn't matter anymore as you have implemented the detection here. Still I'd prefer consistency.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

separatorByte (= 0xFF) has the advantage that it is not a valid byte value anywhere in a UTF-8 string, while 0x00 is (I believe). It is also consistently used in other hashing in Prometheus code.

The main problem there is that if you're debugging and print in the wrong place, things explode. Also, if you ever want to use these keys to binary search, which may make sense in certain cases, you'll have to generate one with a different separator that doesn't mess with tuple-oriented sort order.

0x00 separated records are near standard. I'm really surprised that one would go with anything else. Where else in the code base is this used?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is about hashing, not about tuples. Only add this character while creating the hash, nowhere else.

https://github.com/prometheus/common/blob/master/model/signature.go

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good sir, we are hashing tuples.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For certain definition of tuples, certainly. But we are not modifying the "tuple". The separator byte is only added during hashing, there is no way it will ever be printed or used in sorting.
The null byte is a valid UTF-8 character. I could put it legally into a label value. The 0xFF character can never be part of a valid UTF-8 string. That qualifies it as a good separator character during hashing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 0xFF character can never be part of a valid UTF-8 string. That qualifies it as a good separator character during hashing.

I don't see this link but I've used the constant model.SeparatorByte.

To give you context, we had a sort error in the docker registry API due to the use of a / that sorts earlier than a -. Changing to 0x00 gives you expected, lexical sort order.

h = m.hashAddByte(h, model.SeparatorByte)
}
return h, nil
}
Expand All @@ -231,19 +252,125 @@ func (m *MetricVec) hashLabels(labels Labels) (uint64, error) {
if !ok {
return 0, fmt.Errorf("label name %q missing in label map", label)
}
h = hashAdd(h, val)
h = m.hashAdd(h, val)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above.

h = m.hashAddByte(h, model.SeparatorByte)
}
return h, nil
}

func (m *MetricVec) getOrCreateMetric(hash uint64, labelValues ...string) Metric {
metric, ok := m.children[hash]
// getOrCreateMetric retrieves the metric by hash and label value or creates it
// and returns the new one.
//
// lvs MUST be of type Labels or []string or this method will panic.
//
// This function holds the mutex.
func (m *MetricVec) getOrCreateMetric(hash uint64, lvs interface{}) Metric {
m.mtx.RLock()
metric, ok := m.getMetric(hash, lvs)
m.mtx.RUnlock()
if ok {
return metric
}

m.mtx.Lock()
defer m.mtx.Unlock()
metric, ok = m.getMetric(hash, lvs)
if !ok {
// Copy labelValues. Otherwise, they would be allocated even if we don't go
// down this code path.
copiedLabelValues := append(make([]string, 0, len(labelValues)), labelValues...)
metric = m.newMetric(copiedLabelValues...)
m.children[hash] = metric
lvs := m.copyLabelValues(lvs)
metric = m.newMetric(lvs...)
m.children[hash] = append(m.children[hash], metricWithLabelValues{values: lvs, metric: metric})
}
return metric
}

// getMetric while handling possible collisions in the hash space. Must be
// called while holding read mutex.
//
// lvs must be of type Labels or []string.
func (m *MetricVec) getMetric(h uint64, lvs interface{}) (Metric, bool) {
metrics, ok := m.children[h]
if ok {
return m.selectMetric(metrics, lvs)
}

return nil, false
}

func (m *MetricVec) selectMetric(metrics []metricWithLabelValues, lvs interface{}) (Metric, bool) {
i := m.findMetric(metrics, lvs)

if i < len(metrics) {
return metrics[i].metric, true
}

return nil, false
}

// findMetric returns the index of the matching metric or len(metrics) if not
// found.
func (m *MetricVec) findMetric(metrics []metricWithLabelValues, lvs interface{}) int {
for i, metric := range metrics {
if m.matchLabels(metric.values, lvs) {
return i
}
}

return len(metrics)
}

func (m *MetricVec) matchLabels(values []string, lvs interface{}) bool {
switch lvs := lvs.(type) {
case []string:
if len(values) != len(lvs) {
return false
}

for i, v := range values {
if v != lvs[i] {
return false
}
}

return true
case Labels:
if len(lvs) != len(values) {
return false
}

for i, k := range m.desc.variableLabels {
if values[i] != lvs[k] {
return false
}
}

return true
default:
// If we reach this condition, there is an unexpected type being used
// as a labels value. Either add branch here for the new type or fix
// the bug causing the type to be passed in.
panic("unsupported type")
}
}

// copyLabelValues copies the labels values into common string slice format to
// use when allocating the metric and to keep track of hash collision
// ambiguity.
//
// lvs must be of type Labels or []string or this method will panic.
func (m *MetricVec) copyLabelValues(lvs interface{}) []string {
var labelValues []string
switch lvs := lvs.(type) {
case []string:
labelValues = make([]string, len(lvs))
copy(labelValues, lvs)
case Labels:
labelValues = make([]string, len(lvs))
for i, k := range m.desc.variableLabels {
labelValues[i] = lvs[k]
}
default:
panic(fmt.Sprintf("unsupported type for lvs: %#v", lvs))
}

return labelValues
}
Loading