Skip to content
Closed
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
3 changes: 3 additions & 0 deletions cmd/prometheus/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ func (c *flagConfig) setFeatureListOptions(logger log.Logger) error {
case "extra-scrape-metrics":
c.scrape.ExtraMetrics = true
level.Info(logger).Log("msg", "Experimental additional scrape metrics")
case "share-string-interner":
c.scrape.UseSharedInterner = true
level.Info(logger).Log("msg", "Experimental use of shared string interner in scrape cache")
case "new-service-discovery-manager":
c.enableNewSDManager = true
level.Info(logger).Log("msg", "Experimental service discovery manager")
Expand Down
165 changes: 165 additions & 0 deletions model/intern/intern.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// Copyright 2022 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Inspired / copied / modified from https://gitlab.com/cznic/strutil/blob/master/strutil.go,
// which is MIT licensed, so:
//
// Copyright (c) 2014 The strutil Authors. All rights reserved.

package intern

import (
"sync"

"github.com/prometheus/client_golang/prometheus"
"go.uber.org/atomic"

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

// Shared interner
var Global = New(prometheus.DefaultRegisterer)
Copy link
Member

Choose a reason for hiding this comment

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

what reason does this need to be exported? I think we should either pass in the registerer used elsewhere in the code, or if one is not passed just default to use prometheus.DefaultRegisterer

Copy link
Contributor Author

@tpaschalis tpaschalis Feb 11, 2022

Choose a reason for hiding this comment

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

This Global interner is what's being re-used between the scrape and remote packages.

Could you explain your suggestion once more, please? Do you mean leaving it up to the New() function to decide whether we're using the Global interner (if the DefaultRegisterer is passed), or creating another interner (if a different registerer is passed)? Something else?


// Interner is a string interner.
type Interner interface {
// Metrics returns Metrics for the interner.
Metrics() *Metrics

// Intern will intern an input string, returning the interned string as
// a result.
Intern(string) string

// Release removes an interned string from interner.
Release(string)
}

func New(r prometheus.Registerer) Interner {
return &pool{
m: NewMetrics(r),
pool: map[string]*entry{},
}
}

type Metrics struct {
Strings prometheus.Gauge
NoReferenceReleases prometheus.Counter
}

func NewMetrics(r prometheus.Registerer) *Metrics {
var m Metrics
m.Strings = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: "prometheus",
Subsystem: "interner",
Name: "num_strings",
Help: "The current number of interned strings",
})
m.NoReferenceReleases = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: "prometheus",
Subsystem: "interner",
Name: "string_interner_zero_reference_releases_total",
Help: "The number of times release has been called for strings that are not interned.",
})

if r != nil {
r.MustRegister(m.Strings)
r.MustRegister(m.NoReferenceReleases)
}

return &m
}

type pool struct {
m *Metrics

mtx sync.RWMutex
pool map[string]*entry
}

type entry struct {
refs atomic.Int64

s string
}

func newEntry(s string) *entry {
return &entry{s: s}
}

func (p *pool) Metrics() *Metrics { return p.m }

func (p *pool) Intern(s string) string {
if s == "" {
return ""
}

p.mtx.RLock()
interned, ok := p.pool[s]
p.mtx.RUnlock()
if ok {
interned.refs.Inc()
return interned.s
}
p.mtx.Lock()
defer p.mtx.Unlock()
if interned, ok := p.pool[s]; ok {
interned.refs.Inc()
return interned.s
}

p.m.Strings.Inc()
p.pool[s] = newEntry(s)
p.pool[s].refs.Store(1)
return s
}

func (p *pool) Release(s string) {
p.mtx.RLock()
interned, ok := p.pool[s]
p.mtx.RUnlock()

if !ok {
p.m.NoReferenceReleases.Inc()
return
}

refs := interned.refs.Dec()
if refs > 0 {
return
}

p.mtx.Lock()
defer p.mtx.Unlock()
if interned.refs.Load() != 0 {
return
}
p.m.Strings.Dec()
delete(p.pool, s)
}

// Intern is a helper function for interning all label names and values to a
// given interner.
func Intern(interner Interner, lbls labels.Labels) {
for i, l := range lbls {
lbls[i].Name = interner.Intern(l.Name)
lbls[i].Value = interner.Intern(l.Value)
}
}

// Release is a helper function for releasing all label names and values from a
// given interner.
func Release(interner Interner, ls labels.Labels) {
for _, l := range ls {
interner.Release(l.Name)
interner.Release(l.Value)
}
}
26 changes: 13 additions & 13 deletions storage/remote/intern_test.go → model/intern/intern_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
//
// Copyright (c) 2014 The strutil Authors. All rights reserved.

package remote
package intern

import (
"fmt"
Expand All @@ -27,59 +27,59 @@ import (
)

func TestIntern(t *testing.T) {
interner := newPool()
interner := New(nil).(*pool)
testString := "TestIntern"
interner.intern(testString)
interner.Intern(testString)
interned, ok := interner.pool[testString]

require.Equal(t, true, ok)
require.Equal(t, int64(1), interned.refs.Load(), fmt.Sprintf("expected refs to be 1 but it was %d", interned.refs.Load()))
}

func TestIntern_MultiRef(t *testing.T) {
interner := newPool()
interner := New(nil).(*pool)
testString := "TestIntern_MultiRef"

interner.intern(testString)
interner.Intern(testString)
interned, ok := interner.pool[testString]

require.Equal(t, true, ok)
require.Equal(t, int64(1), interned.refs.Load(), fmt.Sprintf("expected refs to be 1 but it was %d", interned.refs.Load()))

interner.intern(testString)
interner.Intern(testString)
interned, ok = interner.pool[testString]

require.Equal(t, true, ok)
require.Equal(t, int64(2), interned.refs.Load(), fmt.Sprintf("expected refs to be 2 but it was %d", interned.refs.Load()))
}

func TestIntern_DeleteRef(t *testing.T) {
interner := newPool()
interner := New(nil).(*pool)
testString := "TestIntern_DeleteRef"

interner.intern(testString)
interner.Intern(testString)
interned, ok := interner.pool[testString]

require.Equal(t, true, ok)
require.Equal(t, int64(1), interned.refs.Load(), fmt.Sprintf("expected refs to be 1 but it was %d", interned.refs.Load()))

interner.release(testString)
interner.Release(testString)
_, ok = interner.pool[testString]
require.Equal(t, false, ok)
}

func TestIntern_MultiRef_Concurrent(t *testing.T) {
interner := newPool()
interner := New(nil).(*pool)
testString := "TestIntern_MultiRef_Concurrent"

interner.intern(testString)
interner.Intern(testString)
interned, ok := interner.pool[testString]
require.Equal(t, true, ok)
require.Equal(t, int64(1), interned.refs.Load(), fmt.Sprintf("expected refs to be 1 but it was %d", interned.refs.Load()))

go interner.release(testString)
go interner.Release(testString)

interner.intern(testString)
interner.Intern(testString)

time.Sleep(time.Millisecond)

Expand Down
5 changes: 3 additions & 2 deletions scrape/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ type Options struct {

// Optional HTTP client options to use when scraping.
HTTPClientOptions []config_util.HTTPClientOption
UseSharedInterner bool
}

// Manager maintains a set of scrape pools and manages start/stop cycles
Expand Down Expand Up @@ -207,7 +208,7 @@ func (m *Manager) reload() {
level.Error(m.logger).Log("msg", "error reloading target set", "err", "invalid config id:"+setName)
continue
}
sp, err := newScrapePool(scrapeConfig, m.append, m.jitterSeed, log.With(m.logger, "scrape_pool", setName), m.opts.ExtraMetrics, m.opts.PassMetadataInContext, m.opts.HTTPClientOptions)
sp, err := newScrapePool(scrapeConfig, m.append, m.jitterSeed, log.With(m.logger, "scrape_pool", setName), m.opts.ExtraMetrics, m.opts.PassMetadataInContext, m.opts.HTTPClientOptions, m.opts.UseSharedInterner)
if err != nil {
level.Error(m.logger).Log("msg", "error creating new scrape pool", "err", err, "scrape_pool", setName)
continue
Expand Down Expand Up @@ -280,7 +281,7 @@ func (m *Manager) ApplyConfig(cfg *config.Config) error {
sp.stop()
delete(m.scrapePools, name)
} else if !reflect.DeepEqual(sp.config, cfg) {
err := sp.reload(cfg)
err := sp.reload(cfg, m.opts.UseSharedInterner)
if err != nil {
level.Error(m.logger).Log("msg", "error reloading scrape pool", "err", err, "scrape_pool", name)
failed = true
Expand Down
Loading