Skip to content
Open
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
87 changes: 82 additions & 5 deletions structure/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,14 @@

package structure // import "github.com/lightstep/go-expohisto/structure"

import "fmt"
import (
"errors"
"fmt"
"strings"

"github.com/lightstep/go-expohisto/mapping/exponent"
"github.com/lightstep/go-expohisto/mapping/logarithm"
)

// DefaultMaxSize is the default maximum number of buckets per
// positive or negative number range. The value 160 is specified by
Expand All @@ -33,9 +40,17 @@ const MinSize = 2
// of giant histograms.
const MaximumMaxSize = 16384

const DefaultMaxScale = logarithm.MaxScale
const MinScale = exponent.MinScale
const MaximumMaxScale = logarithm.MaxScale

// Config contains configuration for exponential histogram creation.
type Config struct {
maxSize int32
// the hasMaxScale boolean is used to apply the default value for maxScale if none has
// been set
hasMaxScale bool
maxScale int32
}

// Option is the interface that applies a configuration option.
Expand All @@ -59,6 +74,21 @@ func (ms maxSize) apply(cfg Config) Config {
return cfg
}

// WithMaxScale sets the maximum scale of the histogram.
func WithMaxScale(scale int32) Option {
return maxScale(scale)
}

// maxSize is an option to set the maximum histogram size.
type maxScale int32

// apply implements Option.
func (ms maxScale) apply(cfg Config) Config {
cfg.maxScale = int32(ms)
cfg.hasMaxScale = true
return cfg
}

// NewConfig returns an exponential histogram configuration with
// defaults and limits applied.
func NewConfig(opts ...Option) Config {
Expand All @@ -79,12 +109,25 @@ func (c Config) Valid() bool {
// boolean indicating whether the the input was a valid
// configurations.
func (c Config) Validate() (Config, error) {
if c.maxSize >= MinSize && c.maxSize <= MaximumMaxSize {
return c, nil
var errs []error

if err := c.validateMaxSize(); err != nil {
errs = append(errs, err)
}
if err := c.validateMaxScale(); err != nil {
errs = append(errs, err)
}

// not using errors.Join() as that requires Go 1.20
return c, joinErrors(errs)
}

func (c *Config) validateMaxSize() error {
if c.maxSize == 0 {
c.maxSize = DefaultMaxSize
return c, nil
}
if c.maxSize >= MinSize && c.maxSize <= MaximumMaxSize {
return nil
}
err := fmt.Errorf("invalid histogram size: %d", c.maxSize)
if c.maxSize < 0 {
Expand All @@ -94,5 +137,39 @@ func (c Config) Validate() (Config, error) {
} else if c.maxSize > MaximumMaxSize {
c.maxSize = MaximumMaxSize
}
return c, err

return err
}

func (c *Config) validateMaxScale() error {
if !c.hasMaxScale {
c.hasMaxScale = true
c.maxScale = DefaultMaxScale
}
if c.maxScale >= MinScale && c.maxScale <= MaximumMaxScale {
return nil
}
err := fmt.Errorf("invalid histogram max scale: %d", c.maxScale)
if c.maxScale < MinScale {
c.maxScale = MinScale
} else if c.maxScale > MaximumMaxScale {
c.maxScale = MaximumMaxSize
}

return err
}

func joinErrors(errs []error) error {
switch len(errs) {
case 0:
return nil
case 1:
return errs[0]
default:
errStrings := make([]string, len(errs))
for i, err := range errs {
errStrings[i] = err.Error()
}
return errors.New(strings.Join(errStrings, ", "))
}
}
35 changes: 32 additions & 3 deletions structure/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,40 @@ import (
func TestConfigValid(t *testing.T) {
require.True(t, Config{}.Valid())
require.True(t, NewConfig().Valid())
require.True(t, NewConfig(WithMaxSize(MinSize)).Valid())
require.True(t, NewConfig(WithMaxSize(MaximumMaxSize)).Valid())
require.True(t, NewConfig(WithMaxSize((MinSize+MaximumMaxSize)/2)).Valid())
require.True(t, NewConfig(WithMaxSize(MinSize), WithMaxScale(MinScale)).Valid())
require.True(t, NewConfig(WithMaxSize(MaximumMaxSize), WithMaxScale(MaximumMaxScale)).Valid())
require.True(t, NewConfig(WithMaxSize((MinSize+MaximumMaxSize)/2), WithMaxScale((MinScale+MaximumMaxScale)/2)).Valid())

require.False(t, NewConfig(WithMaxSize(-1)).Valid())
require.False(t, NewConfig(WithMaxSize(1<<20)).Valid())
require.False(t, NewConfig(WithMaxSize(1)).Valid())

require.False(t, NewConfig(WithMaxScale(-100)).Valid())
require.False(t, NewConfig(WithMaxScale(100)).Valid())
}

func TestConfigOptions(t *testing.T) {
// zero value defaults
{
c, err := Config{}.Validate()
require.NoError(t, err)
require.Equal(t, DefaultMaxSize, c.maxSize)
require.Equal(t, DefaultMaxScale, c.maxScale)
}

// NewConfig() value defaults
{
c, err := NewConfig().Validate()
require.NoError(t, err)
require.Equal(t, DefaultMaxSize, c.maxSize)
require.Equal(t, DefaultMaxScale, c.maxScale)
}

// options
{
c, err := NewConfig(WithMaxSize(100), WithMaxScale(4)).Validate()
require.NoError(t, err)
require.Equal(t, int32(100), c.maxSize)
require.Equal(t, int32(4), c.maxScale)
}
}
9 changes: 7 additions & 2 deletions structure/exponential.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ type (
// Copy and Move.
maxSize int32

// maxScale is the maximum scale factor. it is set by
// Init(), preserved by Copy and Move.
maxScale int32

// sum is the sum of all Updates reflected in the
// aggregator. It has the same type number as the
// corresponding sdkinstrument.Descriptor.
Expand Down Expand Up @@ -160,8 +164,9 @@ func (h *Histogram[N]) Init(cfg Config) {
cfg, _ = cfg.Validate()

h.maxSize = cfg.maxSize
h.maxScale = cfg.maxScale

m, _ := newMapping(logarithm.MaxScale)
m, _ := newMapping(h.maxScale)
h.mapping = m
}

Expand Down Expand Up @@ -249,7 +254,7 @@ func (h *Histogram[N]) Clear() {
h.zeroCount = 0
h.min = 0
h.max = 0
h.mapping, _ = newMapping(logarithm.MaxScale)
h.mapping, _ = newMapping(h.maxScale)
}

// clear zeros the backing array.
Expand Down
11 changes: 11 additions & 0 deletions structure/exponential_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,17 @@ func TestZeroCountByIncr(t *testing.T) {
requireEqual(t, h1, h2)
}

// Tests that the maxScale option is respected
func TestMaxScale(t *testing.T) {
agg := NewFloat64(NewConfig(WithMaxScale(0)))
agg.Update(4)
agg.Update(1)

require.Equal(t, int32(-1), agg.Positive().Offset())
require.Equal(t, int32(0), agg.Scale())
require.Equal(t, []uint64{1, 0, 1}, getCounts(agg.Positive()))
}

// Benchmarks the Update() function for values in the range [1,2).
func BenchmarkLinear(b *testing.B) {
src := rand.NewSource(77777677777)
Expand Down