Skip to content

Commit 4db77b0

Browse files
committed
metricvec: refactor collision handling to handle equality
After increasing unit test coverage, it was found that the split function call nature of metric matching wasn't working well in many cases. By increasing test coverage, we've ensured that both the fast path and fallback collision path are working appropriately. With these changes, there is a further performance hit, but now the results are ensured to be correct. Signed-off-by: Stephen J Day <[email protected]>
1 parent 3cf50db commit 4db77b0

File tree

2 files changed

+193
-75
lines changed

2 files changed

+193
-75
lines changed

prometheus/vec.go

Lines changed: 107 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ package prometheus
1616
import (
1717
"fmt"
1818
"sync"
19+
20+
"github.com/prometheus/common/model"
1921
)
2022

2123
// MetricVec is a Collector to bundle metrics of the same name that
@@ -25,25 +27,29 @@ import (
2527
// provided in this package.
2628
type MetricVec struct {
2729
mtx sync.RWMutex // Protects the children.
28-
children map[uint64][]metricLabelValue
30+
children map[uint64][]metricWithLabelValues
2931
desc *Desc
3032

31-
newMetric func(labelValues ...string) Metric
32-
hashAdd func(h uint64, s string) uint64 // replace hash function for testing collision handling
33+
newMetric func(labelValues ...string) Metric
34+
hashAdd func(h uint64, s string) uint64 // replace hash function for testing collision handling
35+
hashAddByte func(h uint64, b byte) uint64
3336
}
3437

3538
// newMetricVec returns an initialized MetricVec. The concrete value is
3639
// returned for embedding into another struct.
3740
func newMetricVec(desc *Desc, newMetric func(lvs ...string) Metric) MetricVec {
3841
return MetricVec{
39-
children: map[uint64][]metricLabelValue{},
40-
desc: desc,
41-
newMetric: newMetric,
42-
hashAdd: hashAdd,
42+
children: map[uint64][]metricWithLabelValues{},
43+
desc: desc,
44+
newMetric: newMetric,
45+
hashAdd: hashAdd,
46+
hashAddByte: hashAddByte,
4347
}
4448
}
4549

46-
type metricLabelValue struct {
50+
// metricWithLabelValues provides the metric and its label values for
51+
// disambiguation on hash collision.
52+
type metricWithLabelValues struct {
4753
values []string
4854
metric Metric
4955
}
@@ -96,16 +102,7 @@ func (m *MetricVec) GetMetricWithLabelValues(lvs ...string) (Metric, error) {
96102
return nil, err
97103
}
98104

99-
m.mtx.RLock()
100-
metric, ok := m.getMetric(h, lvs...)
101-
m.mtx.RUnlock()
102-
if ok {
103-
return metric, nil
104-
}
105-
106-
m.mtx.Lock()
107-
defer m.mtx.Unlock()
108-
return m.getOrCreateMetric(h, lvs...), nil
105+
return m.getOrCreateMetric(h, lvs), nil
109106
}
110107

111108
// GetMetricWith returns the Metric for the given Labels map (the label names
@@ -126,21 +123,7 @@ func (m *MetricVec) GetMetricWith(labels Labels) (Metric, error) {
126123
return nil, err
127124
}
128125

129-
m.mtx.RLock()
130-
metrics, ok := m.children[h]
131-
if ok && len(metrics) == 1 {
132-
m.mtx.RUnlock()
133-
return metrics[0].metric, nil
134-
}
135-
m.mtx.RUnlock()
136-
137-
lvs := make([]string, len(labels))
138-
for i, label := range m.desc.variableLabels {
139-
lvs[i] = labels[label]
140-
}
141-
m.mtx.Lock()
142-
defer m.mtx.Unlock()
143-
return m.getOrCreateMetric(h, lvs...), nil
126+
return m.getOrCreateMetric(h, labels), nil
144127
}
145128

146129
// WithLabelValues works as GetMetricWithLabelValues, but panics if an error
@@ -188,7 +171,7 @@ func (m *MetricVec) DeleteLabelValues(lvs ...string) bool {
188171
if err != nil {
189172
return false
190173
}
191-
return m.deleteByHash(h, lvs...)
174+
return m.deleteByHash(h, lvs)
192175
}
193176

194177
// Delete deletes the metric where the variable labels are the same as those
@@ -210,28 +193,21 @@ func (m *MetricVec) Delete(labels Labels) bool {
210193
return false
211194
}
212195

213-
var lvs []string
214-
for _, k := range m.desc.variableLabels {
215-
lvs = append(lvs, labels[k])
216-
}
217-
218-
return m.deleteByHash(h, lvs...)
196+
return m.deleteByHash(h, labels)
219197
}
220198

221199
// deleteByHash removes the metric from the hash bucket h. If there are
222200
// multiple matches in the bucket, use lvs to select a metric and remove only
223201
// that metric.
224-
func (m *MetricVec) deleteByHash(h uint64, lvs ...string) bool {
202+
//
203+
// lvs MUST be of type Labels or []string or this method will panic.
204+
func (m *MetricVec) deleteByHash(h uint64, values interface{}) bool {
225205
metrics, ok := m.children[h]
226206
if !ok {
227207
return false
228208
}
229209

230-
if len(metrics) < 2 {
231-
delete(m.children, h)
232-
}
233-
234-
i := findMetric(lvs, metrics)
210+
i := m.findMetric(metrics, values)
235211
if i >= len(metrics) {
236212
return false
237213
}
@@ -261,6 +237,7 @@ func (m *MetricVec) hashLabelValues(vals []string) (uint64, error) {
261237
h := hashNew()
262238
for _, val := range vals {
263239
h = m.hashAdd(h, val)
240+
h = m.hashAddByte(h, model.SeparatorByte)
264241
}
265242
return h, nil
266243
}
@@ -276,43 +253,51 @@ func (m *MetricVec) hashLabels(labels Labels) (uint64, error) {
276253
return 0, fmt.Errorf("label name %q missing in label map", label)
277254
}
278255
h = m.hashAdd(h, val)
256+
h = m.hashAddByte(h, model.SeparatorByte)
279257
}
280258
return h, nil
281259
}
282260

283-
func (m *MetricVec) getOrCreateMetric(hash uint64, labelValues ...string) Metric {
284-
metric, ok := m.getMetric(hash, labelValues...)
261+
// getOrCreateMetric retrieves the metric by hash and label value or creates it
262+
// and returns the new one.
263+
//
264+
// lvs MUST be of type Labels or []string or this method will panic.
265+
//
266+
// This function holds the mutex.
267+
func (m *MetricVec) getOrCreateMetric(hash uint64, lvs interface{}) Metric {
268+
m.mtx.RLock()
269+
metric, ok := m.getMetric(hash, lvs)
270+
m.mtx.RUnlock()
271+
if ok {
272+
return metric
273+
}
274+
275+
m.mtx.Lock()
276+
defer m.mtx.Unlock()
277+
metric, ok = m.getMetric(hash, lvs)
285278
if !ok {
286-
// Copy labelValues. Otherwise, they would be allocated even if we don't go
287-
// down this code path.
288-
copiedLabelValues := append(make([]string, 0, len(labelValues)), labelValues...)
289-
metric = m.newMetric(copiedLabelValues...)
290-
m.children[hash] = append(m.children[hash], metricLabelValue{values: copiedLabelValues, metric: metric})
279+
lvs := m.copyLabelValues(lvs)
280+
metric = m.newMetric(lvs...)
281+
m.children[hash] = append(m.children[hash], metricWithLabelValues{values: lvs, metric: metric})
291282
}
292283
return metric
293284
}
294285

295286
// getMetric while handling possible collisions in the hash space. Must be
296287
// called while holding read mutex.
297-
func (m *MetricVec) getMetric(h uint64, lvs ...string) (Metric, bool) {
288+
//
289+
// lvs must be of type Labels or []string.
290+
func (m *MetricVec) getMetric(h uint64, lvs interface{}) (Metric, bool) {
298291
metrics, ok := m.children[h]
299292
if ok {
300-
return m.selectMetric(lvs, metrics)
293+
return m.selectMetric(metrics, lvs)
301294
}
302295

303296
return nil, false
304297
}
305298

306-
func (m *MetricVec) selectMetric(lvs []string, metrics []metricLabelValue) (Metric, bool) {
307-
switch len(metrics) {
308-
case 0:
309-
return nil, false
310-
case 1:
311-
// collisions are rare, this should be the fast path.
312-
return metrics[0].metric, true
313-
}
314-
315-
i := findMetric(lvs, metrics)
299+
func (m *MetricVec) selectMetric(metrics []metricWithLabelValues, lvs interface{}) (Metric, bool) {
300+
i := m.findMetric(metrics, lvs)
316301

317302
if i < len(metrics) {
318303
return metrics[i].metric, true
@@ -323,22 +308,69 @@ func (m *MetricVec) selectMetric(lvs []string, metrics []metricLabelValue) (Metr
323308

324309
// findMetric returns the index of the matching metric or len(metrics) if not
325310
// found.
326-
func findMetric(lvs []string, metrics []metricLabelValue) int {
327-
next:
311+
func (m *MetricVec) findMetric(metrics []metricWithLabelValues, lvs interface{}) int {
328312
for i, metric := range metrics {
329-
if len(metric.values) != len(lvs) {
330-
continue
313+
if m.matchLabels(metric.values, lvs) {
314+
return i
331315
}
316+
}
317+
318+
return len(metrics)
319+
}
332320

333-
for j, v := range metric.values {
334-
if v != lvs[j] {
335-
continue next
321+
func (m *MetricVec) matchLabels(values []string, lvs interface{}) bool {
322+
switch lvs := lvs.(type) {
323+
case []string:
324+
if len(values) != len(lvs) {
325+
return false
326+
}
327+
328+
for i, v := range values {
329+
if v != lvs[i] {
330+
return false
331+
}
332+
}
333+
334+
return true
335+
case Labels:
336+
if len(lvs) != len(values) {
337+
return false
338+
}
339+
340+
for i, k := range m.desc.variableLabels {
341+
if values[i] != lvs[k] {
342+
return false
336343
}
337344
}
338-
// falling out of the loop here means we have a match!
339345

340-
return i
346+
return true
347+
default:
348+
// If we reach this condition, there is an unexpected type being used
349+
// as a labels value. Either add branch here for the new type or fix
350+
// the bug causing the type to be passed in.
351+
panic("unsupported type")
341352
}
353+
}
342354

343-
return len(metrics)
355+
// copyLabelValues copies the labels values into common string slice format to
356+
// use when allocating the metric and to keep track of hash collision
357+
// ambiguity.
358+
//
359+
// lvs must be of type Labels or []string or this method will panic.
360+
func (m *MetricVec) copyLabelValues(lvs interface{}) []string {
361+
var labelValues []string
362+
switch lvs := lvs.(type) {
363+
case []string:
364+
labelValues = make([]string, len(lvs))
365+
copy(labelValues, lvs)
366+
case Labels:
367+
labelValues = make([]string, len(lvs))
368+
for i, k := range m.desc.variableLabels {
369+
labelValues[i] = lvs[k]
370+
}
371+
default:
372+
panic(fmt.Sprintf("unsupported type for lvs: %#v", lvs))
373+
}
374+
375+
return labelValues
344376
}

0 commit comments

Comments
 (0)