Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Do not allocate memory when there's no constraints #1296

Merged
merged 1 commit into from Jun 27, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
131 changes: 88 additions & 43 deletions prometheus/benchmark_test.go
Expand Up @@ -18,18 +18,94 @@ import (
"testing"
)

func BenchmarkCounterWithLabelValues(b *testing.B) {
m := NewCounterVec(
CounterOpts{
Name: "benchmark_counter",
Help: "A counter to benchmark it.",
},
[]string{"one", "two", "three"},
)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
m.WithLabelValues("eins", "zwei", "drei").Inc()
func BenchmarkCounter(b *testing.B) {
type fns []func(*CounterVec) Counter

twoConstraint := func(_ string) string {
return "two"
}

deLV := func(m *CounterVec) Counter {
return m.WithLabelValues("eins", "zwei", "drei")
}
frLV := func(m *CounterVec) Counter {
return m.WithLabelValues("une", "deux", "trois")
}
nlLV := func(m *CounterVec) Counter {
return m.WithLabelValues("een", "twee", "drie")
}

deML := func(m *CounterVec) Counter {
return m.With(Labels{"two": "zwei", "one": "eins", "three": "drei"})
}
frML := func(m *CounterVec) Counter {
return m.With(Labels{"two": "deux", "one": "une", "three": "trois"})
}
nlML := func(m *CounterVec) Counter {
return m.With(Labels{"two": "twee", "one": "een", "three": "drie"})
}

deLabels := Labels{"two": "zwei", "one": "eins", "three": "drei"}
dePML := func(m *CounterVec) Counter {
return m.With(deLabels)
}
frLabels := Labels{"two": "deux", "one": "une", "three": "trois"}
frPML := func(m *CounterVec) Counter {
return m.With(frLabels)
}
nlLabels := Labels{"two": "twee", "one": "een", "three": "drie"}
nlPML := func(m *CounterVec) Counter {
return m.With(nlLabels)
}

table := []struct {
name string
constraint LabelConstraint
counters fns
}{
{"With Label Values", nil, fns{deLV}},
{"With Label Values and Constraint", twoConstraint, fns{deLV}},
{"With triple Label Values", nil, fns{deLV, frLV, nlLV}},
{"With triple Label Values and Constraint", twoConstraint, fns{deLV, frLV, nlLV}},
{"With repeated Label Values", nil, fns{deLV, deLV}},
{"With repeated Label Values and Constraint", twoConstraint, fns{deLV, deLV}},
{"With Mapped Labels", nil, fns{deML}},
{"With Mapped Labels and Constraint", twoConstraint, fns{deML}},
{"With triple Mapped Labels", nil, fns{deML, frML, nlML}},
{"With triple Mapped Labels and Constraint", twoConstraint, fns{deML, frML, nlML}},
{"With repeated Mapped Labels", nil, fns{deML, deML}},
{"With repeated Mapped Labels and Constraint", twoConstraint, fns{deML, deML}},
{"With Prepared Mapped Labels", nil, fns{dePML}},
{"With Prepared Mapped Labels and Constraint", twoConstraint, fns{dePML}},
{"With triple Prepared Mapped Labels", nil, fns{dePML, frPML, nlPML}},
{"With triple Prepared Mapped Labels and Constraint", twoConstraint, fns{dePML, frPML, nlPML}},
{"With repeated Prepared Mapped Labels", nil, fns{dePML, dePML}},
{"With repeated Prepared Mapped Labels and Constraint", twoConstraint, fns{dePML, dePML}},
}

for _, t := range table {
b.Run(t.name, func(b *testing.B) {
m := V2.NewCounterVec(
CounterVecOpts{
CounterOpts: CounterOpts{
Name: "benchmark_counter",
Help: "A counter to benchmark it.",
},
VariableLabels: ConstrainedLabels{
ConstrainedLabel{Name: "one"},
ConstrainedLabel{Name: "two", Constraint: t.constraint},
ConstrainedLabel{Name: "three"},
},
},
)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
for _, fn := range t.counters {
fn(m).Inc()
}
}
})
}
}

Expand All @@ -56,37 +132,6 @@ func BenchmarkCounterWithLabelValuesConcurrent(b *testing.B) {
wg.Wait()
}

func BenchmarkCounterWithMappedLabels(b *testing.B) {
m := NewCounterVec(
CounterOpts{
Name: "benchmark_counter",
Help: "A counter to benchmark it.",
},
[]string{"one", "two", "three"},
)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
m.With(Labels{"two": "zwei", "one": "eins", "three": "drei"}).Inc()
}
}

func BenchmarkCounterWithPreparedMappedLabels(b *testing.B) {
m := NewCounterVec(
CounterOpts{
Name: "benchmark_counter",
Help: "A counter to benchmark it.",
},
[]string{"one", "two", "three"},
)
b.ReportAllocs()
b.ResetTimer()
labels := Labels{"two": "zwei", "one": "eins", "three": "drei"}
for i := 0; i < b.N; i++ {
m.With(labels).Inc()
}
}

func BenchmarkCounterNoLabels(b *testing.B) {
m := NewCounter(CounterOpts{
Name: "benchmark_counter",
Expand Down
4 changes: 2 additions & 2 deletions prometheus/counter.go
Expand Up @@ -202,8 +202,8 @@ func (v2) NewCounterVec(opts CounterVecOpts) *CounterVec {
)
return &CounterVec{
MetricVec: NewMetricVec(desc, func(lvs ...string) Metric {
if len(lvs) != len(desc.variableLabels) {
panic(makeInconsistentCardinalityError(desc.fqName, desc.variableLabels.labelNames(), lvs))
if len(lvs) != len(desc.variableLabels.names) {
panic(makeInconsistentCardinalityError(desc.fqName, desc.variableLabels.names, lvs))
}
result := &counter{desc: desc, labelPairs: MakeLabelPairs(desc, lvs), now: time.Now}
result.init(result) // Init self-collection.
Expand Down
28 changes: 18 additions & 10 deletions prometheus/desc.go
Expand Up @@ -52,7 +52,7 @@ type Desc struct {
constLabelPairs []*dto.LabelPair
// variableLabels contains names of labels and normalization function for
// which the metric maintains variable values.
variableLabels ConstrainedLabels
variableLabels *compiledLabels
// id is a hash of the values of the ConstLabels and fqName. This
// must be unique among all registered descriptors and can therefore be
// used as an identifier of the descriptor.
Expand Down Expand Up @@ -93,7 +93,7 @@ func (v2) NewDesc(fqName, help string, variableLabels ConstrainableLabels, const
d := &Desc{
fqName: fqName,
help: help,
variableLabels: variableLabels.constrainedLabels(),
variableLabels: variableLabels.compile(),
}
if !model.IsValidMetricName(model.LabelValue(fqName)) {
d.err = fmt.Errorf("%q is not a valid metric name", fqName)
Expand All @@ -103,7 +103,7 @@ func (v2) NewDesc(fqName, help string, variableLabels ConstrainableLabels, const
// their sorted label names) plus the fqName (at position 0).
labelValues := make([]string, 1, len(constLabels)+1)
labelValues[0] = fqName
labelNames := make([]string, 0, len(constLabels)+len(d.variableLabels))
labelNames := make([]string, 0, len(constLabels)+len(d.variableLabels.names))
labelNameSet := map[string]struct{}{}
// First add only the const label names and sort them...
for labelName := range constLabels {
Expand All @@ -128,13 +128,13 @@ func (v2) NewDesc(fqName, help string, variableLabels ConstrainableLabels, const
// Now add the variable label names, but prefix them with something that
// cannot be in a regular label name. That prevents matching the label
// dimension with a different mix between preset and variable labels.
for _, label := range d.variableLabels {
if !checkLabelName(label.Name) {
d.err = fmt.Errorf("%q is not a valid label name for metric %q", label.Name, fqName)
for _, label := range d.variableLabels.names {
if !checkLabelName(label) {
d.err = fmt.Errorf("%q is not a valid label name for metric %q", label, fqName)
return d
}
labelNames = append(labelNames, "$"+label.Name)
labelNameSet[label.Name] = struct{}{}
labelNames = append(labelNames, "$"+label)
labelNameSet[label] = struct{}{}
}
if len(labelNames) != len(labelNameSet) {
d.err = fmt.Errorf("duplicate label names in constant and variable labels for metric %q", fqName)
Expand Down Expand Up @@ -189,11 +189,19 @@ func (d *Desc) String() string {
fmt.Sprintf("%s=%q", lp.GetName(), lp.GetValue()),
)
}
vlStrings := make([]string, 0, len(d.variableLabels.names))
for _, vl := range d.variableLabels.names {
if fn, ok := d.variableLabels.labelConstraints[vl]; ok && fn != nil {
vlStrings = append(vlStrings, fmt.Sprintf("c(%s)", vl))
} else {
vlStrings = append(vlStrings, vl)
}
}
return fmt.Sprintf(
"Desc{fqName: %q, help: %q, constLabels: {%s}, variableLabels: %v}",
"Desc{fqName: %q, help: %q, constLabels: {%s}, variableLabels: {%s}}",
d.fqName,
d.help,
strings.Join(lpStrings, ","),
d.variableLabels,
strings.Join(vlStrings, ","),
)
}
4 changes: 2 additions & 2 deletions prometheus/examples_test.go
Expand Up @@ -292,9 +292,9 @@ func ExampleRegister() {

// Output:
// taskCounter registered.
// taskCounterVec not registered: a previously registered descriptor with the same fully-qualified name as Desc{fqName: "worker_pool_completed_tasks_total", help: "Total number of tasks completed.", constLabels: {}, variableLabels: [{worker_id <nil>}]} has different label names or a different help string
// taskCounterVec not registered: a previously registered descriptor with the same fully-qualified name as Desc{fqName: "worker_pool_completed_tasks_total", help: "Total number of tasks completed.", constLabels: {}, variableLabels: {worker_id}} has different label names or a different help string
// taskCounter unregistered.
// taskCounterVec not registered: a previously registered descriptor with the same fully-qualified name as Desc{fqName: "worker_pool_completed_tasks_total", help: "Total number of tasks completed.", constLabels: {}, variableLabels: [{worker_id <nil>}]} has different label names or a different help string
// taskCounterVec not registered: a previously registered descriptor with the same fully-qualified name as Desc{fqName: "worker_pool_completed_tasks_total", help: "Total number of tasks completed.", constLabels: {}, variableLabels: {worker_id}} has different label names or a different help string
// taskCounterVec registered.
// Worker initialization failed: inconsistent label cardinality: expected 1 label values but got 2 in []string{"42", "spurious arg"}
// notMyCounter is nil.
Expand Down
2 changes: 1 addition & 1 deletion prometheus/expvar_collector.go
Expand Up @@ -48,7 +48,7 @@ func (e *expvarCollector) Collect(ch chan<- Metric) {
continue
}
var v interface{}
labels := make([]string, len(desc.variableLabels))
labels := make([]string, len(desc.variableLabels.names))
if err := json.Unmarshal([]byte(expVar.String()), &v); err != nil {
ch <- NewInvalidMetric(desc, err)
continue
Expand Down
4 changes: 2 additions & 2 deletions prometheus/gauge.go
Expand Up @@ -166,8 +166,8 @@ func (v2) NewGaugeVec(opts GaugeVecOpts) *GaugeVec {
)
return &GaugeVec{
MetricVec: NewMetricVec(desc, func(lvs ...string) Metric {
if len(lvs) != len(desc.variableLabels) {
panic(makeInconsistentCardinalityError(desc.fqName, desc.variableLabels.labelNames(), lvs))
if len(lvs) != len(desc.variableLabels.names) {
panic(makeInconsistentCardinalityError(desc.fqName, desc.variableLabels.names, lvs))
}
result := &gauge{desc: desc, labelPairs: MakeLabelPairs(desc, lvs)}
result.init(result) // Init self-collection.
Expand Down
2 changes: 1 addition & 1 deletion prometheus/gauge_test.go
Expand Up @@ -170,7 +170,7 @@ func TestGaugeFunc(t *testing.T) {
func() float64 { return 3.1415 },
)

if expected, got := `Desc{fqName: "test_name", help: "test help", constLabels: {a="1",b="2"}, variableLabels: []}`, gf.Desc().String(); expected != got {
if expected, got := `Desc{fqName: "test_name", help: "test help", constLabels: {a="1",b="2"}, variableLabels: {}}`, gf.Desc().String(); expected != got {
t.Errorf("expected %q, got %q", expected, got)
}

Expand Down
10 changes: 5 additions & 5 deletions prometheus/histogram.go
Expand Up @@ -499,12 +499,12 @@ func NewHistogram(opts HistogramOpts) Histogram {
}

func newHistogram(desc *Desc, opts HistogramOpts, labelValues ...string) Histogram {
if len(desc.variableLabels) != len(labelValues) {
panic(makeInconsistentCardinalityError(desc.fqName, desc.variableLabels.labelNames(), labelValues))
if len(desc.variableLabels.names) != len(labelValues) {
panic(makeInconsistentCardinalityError(desc.fqName, desc.variableLabels.names, labelValues))
}

for _, n := range desc.variableLabels {
if n.Name == bucketLabel {
for _, n := range desc.variableLabels.names {
if n == bucketLabel {
panic(errBucketLabelNotAllowed)
}
}
Expand Down Expand Up @@ -1230,7 +1230,7 @@ func NewConstHistogram(
if desc.err != nil {
return nil, desc.err
}
if err := validateLabelValues(labelValues, len(desc.variableLabels)); err != nil {
if err := validateLabelValues(labelValues, len(desc.variableLabels.names)); err != nil {
return nil, err
}
return &constHistogram{
Expand Down
58 changes: 42 additions & 16 deletions prometheus/labels.go
Expand Up @@ -32,19 +32,15 @@ import (
// create a Desc.
type Labels map[string]string

// LabelConstraint normalizes label values.
type LabelConstraint func(string) string

// ConstrainedLabels represents a label name and its constrain function
// to normalize label values. This type is commonly used when constructing
// metric vector Collectors.
type ConstrainedLabel struct {
Name string
Constraint func(string) string
}

func (cl ConstrainedLabel) Constrain(v string) string {
if cl.Constraint == nil {
return v
}
return cl.Constraint(v)
Constraint LabelConstraint
}

// ConstrainableLabels is an interface that allows creating of labels that can
Expand All @@ -58,7 +54,7 @@ func (cl ConstrainedLabel) Constrain(v string) string {
// },
// })
type ConstrainableLabels interface {
constrainedLabels() ConstrainedLabels
compile() *compiledLabels
labelNames() []string
}

Expand All @@ -67,8 +63,20 @@ type ConstrainableLabels interface {
// metric vector Collectors.
type ConstrainedLabels []ConstrainedLabel

func (cls ConstrainedLabels) constrainedLabels() ConstrainedLabels {
return cls
func (cls ConstrainedLabels) compile() *compiledLabels {
compiled := &compiledLabels{
names: make([]string, len(cls)),
labelConstraints: map[string]LabelConstraint{},
}

for i, label := range cls {
compiled.names[i] = label.Name
if label.Constraint != nil {
compiled.labelConstraints[label.Name] = label.Constraint
}
}

return compiled
}

func (cls ConstrainedLabels) labelNames() []string {
Expand All @@ -92,18 +100,36 @@ func (cls ConstrainedLabels) labelNames() []string {
// }
type UnconstrainedLabels []string

func (uls UnconstrainedLabels) constrainedLabels() ConstrainedLabels {
constrainedLabels := make([]ConstrainedLabel, len(uls))
for i, l := range uls {
constrainedLabels[i] = ConstrainedLabel{Name: l}
func (uls UnconstrainedLabels) compile() *compiledLabels {
return &compiledLabels{
names: uls,
}
return constrainedLabels
}

func (uls UnconstrainedLabels) labelNames() []string {
return uls
}

type compiledLabels struct {
names []string
labelConstraints map[string]LabelConstraint
}

func (cls *compiledLabels) compile() *compiledLabels {
return cls
}

func (cls *compiledLabels) labelNames() []string {
return cls.names
}

func (cls *compiledLabels) constrain(labelName, value string) string {
if fn, ok := cls.labelConstraints[labelName]; ok && fn != nil {
return fn(value)
}
return value
}

// reservedLabelPrefix is a prefix which is not legal in user-supplied
// label names.
const reservedLabelPrefix = "__"
Expand Down