1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2025-01-18 03:22:12 +02:00
opentelemetry-go/sdk/metric/stress_test.go
Tyler Yahn 91091b44eb Add comments and test for 64-bit field alignment (#418)
* Add comments on needed filed alignment

Add comment about alignment requirements to all struct fields who's
values are passed to 64-bit atomic operations.

Update any struct's field ordering if one or more of those fields has
alignment requirements to support 64-bit atomic operations.

* Add 64-bit alignment tests

Most `struct` that have field alignment requirements are now statically
validated prior to testing. The only `struct`s not validated that have
these requirements are ones defined in tests themselves where multiple
`TestMain` functions would be needed to test them. Given the fields are
already identified with comments specifying the alignment requirements
and they are in the test themselves, this seems like an OK omission.

Co-authored-by: Liz Fong-Jones <elizabeth@ctyalcove.org>
2020-01-06 13:08:40 -05:00

534 lines
13 KiB
Go

// Copyright 2019, OpenTelemetry 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.
// This test is too large for the race detector. This SDK uses no locks
// that the race detector would help with, anyway.
// +build !race
package metric_test
import (
"context"
"fmt"
"math"
"math/rand"
"runtime"
"sort"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
"go.opentelemetry.io/otel/api/core"
"go.opentelemetry.io/otel/api/key"
"go.opentelemetry.io/otel/api/metric"
api "go.opentelemetry.io/otel/api/metric"
export "go.opentelemetry.io/otel/sdk/export/metric"
"go.opentelemetry.io/otel/sdk/export/metric/aggregator"
sdk "go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/metric/aggregator/counter"
"go.opentelemetry.io/otel/sdk/metric/aggregator/gauge"
)
const (
concurrencyPerCPU = 100
reclaimPeriod = time.Millisecond * 100
testRun = time.Second
epsilon = 1e-10
)
type (
testFixture struct {
// stop has to be aligned for 64-bit atomic operations.
stop int64
expected sync.Map
received sync.Map // Note: doesn't require synchronization
wg sync.WaitGroup
impl testImpl
T *testing.T
lock sync.Mutex
lused map[string]bool
dupCheck map[testKey]int
totalDups int64
}
testKey struct {
labels string
descriptor *export.Descriptor
}
testImpl struct {
newInstrument func(meter api.Meter, name string) withImpl
getUpdateValue func() core.Number
operate func(interface{}, context.Context, core.Number, api.LabelSet)
newStore func() interface{}
// storeCollect and storeExpect are the same for
// counters, different for gauges, to ensure we are
// testing the timestamps correctly.
storeCollect func(store interface{}, value core.Number, ts time.Time)
storeExpect func(store interface{}, value core.Number)
readStore func(store interface{}) core.Number
equalValues func(a, b core.Number) bool
}
withImpl interface {
Impl() metric.InstrumentImpl
}
// gaugeState supports merging gauge values, for the case
// where a race condition causes duplicate records. We always
// take the later timestamp.
gaugeState struct {
// raw has to be aligned for 64-bit atomic operations.
raw core.Number
ts time.Time
}
)
func concurrency() int {
return concurrencyPerCPU * runtime.NumCPU()
}
func canonicalizeLabels(ls []core.KeyValue) string {
copy := append(ls[0:0:0], ls...)
sort.SliceStable(copy, func(i, j int) bool {
return copy[i].Key < copy[j].Key
})
var b strings.Builder
for _, kv := range copy {
b.WriteString(string(kv.Key))
b.WriteString("=")
b.WriteString(kv.Value.Emit())
b.WriteString("$")
}
return b.String()
}
func getPeriod() time.Duration {
dur := math.Max(
float64(reclaimPeriod)/10,
float64(reclaimPeriod)*(1+0.1*rand.NormFloat64()),
)
return time.Duration(dur)
}
func (f *testFixture) someLabels() []core.KeyValue {
n := 1 + rand.Intn(3)
l := make([]core.KeyValue, n)
for {
oused := map[string]bool{}
for i := 0; i < n; i++ {
var k string
for {
k = fmt.Sprint("k", rand.Intn(1000000000))
if !oused[k] {
oused[k] = true
break
}
}
l[i] = key.New(k).String(fmt.Sprint("v", rand.Intn(1000000000)))
}
lc := canonicalizeLabels(l)
f.lock.Lock()
avail := !f.lused[lc]
if avail {
f.lused[lc] = true
f.lock.Unlock()
return l
}
f.lock.Unlock()
}
}
func (f *testFixture) startWorker(sdk *sdk.SDK, wg *sync.WaitGroup, i int) {
ctx := context.Background()
name := fmt.Sprint("test_", i)
instrument := f.impl.newInstrument(sdk, name)
descriptor := sdk.GetDescriptor(instrument.Impl())
kvs := f.someLabels()
clabs := canonicalizeLabels(kvs)
labs := sdk.Labels(kvs...)
dur := getPeriod()
key := testKey{
labels: clabs,
descriptor: descriptor,
}
for {
sleep := time.Duration(rand.ExpFloat64() * float64(dur))
time.Sleep(sleep)
value := f.impl.getUpdateValue()
f.impl.operate(instrument, ctx, value, labs)
actual, _ := f.expected.LoadOrStore(key, f.impl.newStore())
f.impl.storeExpect(actual, value)
if atomic.LoadInt64(&f.stop) != 0 {
wg.Done()
return
}
}
}
func (f *testFixture) assertTest(numCollect int) {
csize := 0
f.received.Range(func(key, gstore interface{}) bool {
csize++
gvalue := f.impl.readStore(gstore)
estore, loaded := f.expected.Load(key)
if !loaded {
f.T.Error("Could not locate expected key: ", key)
}
evalue := f.impl.readStore(estore)
if !f.impl.equalValues(evalue, gvalue) {
f.T.Error("Expected value mismatch: ",
evalue, "!=", gvalue, " for ", key)
}
return true
})
rsize := 0
f.expected.Range(func(key, value interface{}) bool {
rsize++
if _, loaded := f.received.Load(key); !loaded {
f.T.Error("Did not receive expected key: ", key)
}
return true
})
if rsize != csize {
f.T.Error("Did not receive the correct set of metrics: Received != Expected", rsize, csize)
}
// Note: It's useful to know the test triggers this condition,
// but we can't assert it. Infrequently no duplicates are
// found, and we can't really force a race to happen.
//
// fmt.Printf("Test duplicate records seen: %.1f%%\n",
// float64(100*f.totalDups/int64(numCollect*concurrency())))
}
func (f *testFixture) preCollect() {
// Collect calls Process in a single-threaded context. No need
// to lock this struct.
f.dupCheck = map[testKey]int{}
}
func (*testFixture) AggregatorFor(descriptor *export.Descriptor) export.Aggregator {
switch descriptor.MetricKind() {
case export.CounterKind:
return counter.New()
case export.GaugeKind:
return gauge.New()
default:
panic("Not implemented for this test")
}
}
func (*testFixture) CheckpointSet() export.CheckpointSet {
return nil
}
func (*testFixture) FinishedCollection() {
}
func (f *testFixture) Process(_ context.Context, record export.Record) error {
key := testKey{
labels: canonicalizeLabels(record.Labels().Ordered()),
descriptor: record.Descriptor(),
}
if f.dupCheck[key] == 0 {
f.dupCheck[key]++
} else {
f.totalDups++
}
actual, _ := f.received.LoadOrStore(key, f.impl.newStore())
agg := record.Aggregator()
switch record.Descriptor().MetricKind() {
case export.CounterKind:
counter := agg.(aggregator.Sum)
sum, err := counter.Sum()
if err != nil {
f.T.Fatal("Sum error: ", err)
}
f.impl.storeCollect(actual, sum, time.Time{})
case export.GaugeKind:
gauge := agg.(aggregator.LastValue)
lv, ts, err := gauge.LastValue()
if err != nil && err != aggregator.ErrNoLastValue {
f.T.Fatal("Last value error: ", err)
}
f.impl.storeCollect(actual, lv, ts)
default:
panic("Not used in this test")
}
return nil
}
func stressTest(t *testing.T, impl testImpl) {
ctx := context.Background()
t.Parallel()
fixture := &testFixture{
T: t,
impl: impl,
lused: map[string]bool{},
}
cc := concurrency()
sdk := sdk.New(fixture, sdk.NewDefaultLabelEncoder())
fixture.wg.Add(cc + 1)
for i := 0; i < cc; i++ {
go fixture.startWorker(sdk, &fixture.wg, i)
}
numCollect := 0
go func() {
for {
time.Sleep(reclaimPeriod)
fixture.preCollect()
sdk.Collect(ctx)
numCollect++
if atomic.LoadInt64(&fixture.stop) != 0 {
fixture.wg.Done()
return
}
}
}()
time.Sleep(testRun)
atomic.StoreInt64(&fixture.stop, 1)
fixture.wg.Wait()
fixture.preCollect()
sdk.Collect(ctx)
numCollect++
fixture.assertTest(numCollect)
}
func int64sEqual(a, b core.Number) bool {
return a.AsInt64() == b.AsInt64()
}
func float64sEqual(a, b core.Number) bool {
diff := math.Abs(a.AsFloat64() - b.AsFloat64())
return diff < math.Abs(a.AsFloat64())*epsilon
}
// Counters
func intCounterTestImpl(nonMonotonic bool) testImpl {
return testImpl{
newInstrument: func(meter api.Meter, name string) withImpl {
return meter.NewInt64Counter(name, api.WithMonotonic(!nonMonotonic))
},
getUpdateValue: func() core.Number {
var offset int64
if nonMonotonic {
offset = -50
}
for {
x := offset + int64(rand.Intn(100))
if x != 0 {
return core.NewInt64Number(x)
}
}
},
operate: func(inst interface{}, ctx context.Context, value core.Number, labels api.LabelSet) {
counter := inst.(api.Int64Counter)
counter.Add(ctx, value.AsInt64(), labels)
},
newStore: func() interface{} {
n := core.NewInt64Number(0)
return &n
},
storeCollect: func(store interface{}, value core.Number, _ time.Time) {
store.(*core.Number).AddInt64Atomic(value.AsInt64())
},
storeExpect: func(store interface{}, value core.Number) {
store.(*core.Number).AddInt64Atomic(value.AsInt64())
},
readStore: func(store interface{}) core.Number {
return store.(*core.Number).AsNumberAtomic()
},
equalValues: int64sEqual,
}
}
func TestStressInt64CounterNormal(t *testing.T) {
stressTest(t, intCounterTestImpl(false))
}
func TestStressInt64CounterNonMonotonic(t *testing.T) {
stressTest(t, intCounterTestImpl(true))
}
func floatCounterTestImpl(nonMonotonic bool) testImpl {
return testImpl{
newInstrument: func(meter api.Meter, name string) withImpl {
return meter.NewFloat64Counter(name, api.WithMonotonic(!nonMonotonic))
},
getUpdateValue: func() core.Number {
var offset float64
if nonMonotonic {
offset = -0.5
}
for {
x := offset + rand.Float64()
if x != 0 {
return core.NewFloat64Number(x)
}
}
},
operate: func(inst interface{}, ctx context.Context, value core.Number, labels api.LabelSet) {
counter := inst.(api.Float64Counter)
counter.Add(ctx, value.AsFloat64(), labels)
},
newStore: func() interface{} {
n := core.NewFloat64Number(0.0)
return &n
},
storeCollect: func(store interface{}, value core.Number, _ time.Time) {
store.(*core.Number).AddFloat64Atomic(value.AsFloat64())
},
storeExpect: func(store interface{}, value core.Number) {
store.(*core.Number).AddFloat64Atomic(value.AsFloat64())
},
readStore: func(store interface{}) core.Number {
return store.(*core.Number).AsNumberAtomic()
},
equalValues: float64sEqual,
}
}
func TestStressFloat64CounterNormal(t *testing.T) {
stressTest(t, floatCounterTestImpl(false))
}
func TestStressFloat64CounterNonMonotonic(t *testing.T) {
stressTest(t, floatCounterTestImpl(true))
}
// Gauges
func intGaugeTestImpl(monotonic bool) testImpl {
// (Now()-startTime) is used as a free monotonic source
startTime := time.Now()
return testImpl{
newInstrument: func(meter api.Meter, name string) withImpl {
return meter.NewInt64Gauge(name, api.WithMonotonic(monotonic))
},
getUpdateValue: func() core.Number {
if !monotonic {
r1 := rand.Int63()
return core.NewInt64Number(rand.Int63() - r1)
}
return core.NewInt64Number(int64(time.Since(startTime)))
},
operate: func(inst interface{}, ctx context.Context, value core.Number, labels api.LabelSet) {
gauge := inst.(api.Int64Gauge)
gauge.Set(ctx, value.AsInt64(), labels)
},
newStore: func() interface{} {
return &gaugeState{
raw: core.NewInt64Number(0),
}
},
storeCollect: func(store interface{}, value core.Number, ts time.Time) {
gs := store.(*gaugeState)
if !ts.Before(gs.ts) {
gs.ts = ts
gs.raw.SetInt64Atomic(value.AsInt64())
}
},
storeExpect: func(store interface{}, value core.Number) {
gs := store.(*gaugeState)
gs.raw.SetInt64Atomic(value.AsInt64())
},
readStore: func(store interface{}) core.Number {
gs := store.(*gaugeState)
return gs.raw.AsNumberAtomic()
},
equalValues: int64sEqual,
}
}
func TestStressInt64GaugeNormal(t *testing.T) {
stressTest(t, intGaugeTestImpl(false))
}
func TestStressInt64GaugeMonotonic(t *testing.T) {
stressTest(t, intGaugeTestImpl(true))
}
func floatGaugeTestImpl(monotonic bool) testImpl {
// (Now()-startTime) is used as a free monotonic source
startTime := time.Now()
return testImpl{
newInstrument: func(meter api.Meter, name string) withImpl {
return meter.NewFloat64Gauge(name, api.WithMonotonic(monotonic))
},
getUpdateValue: func() core.Number {
if !monotonic {
return core.NewFloat64Number((-0.5 + rand.Float64()) * 100000)
}
return core.NewFloat64Number(float64(time.Since(startTime)))
},
operate: func(inst interface{}, ctx context.Context, value core.Number, labels api.LabelSet) {
gauge := inst.(api.Float64Gauge)
gauge.Set(ctx, value.AsFloat64(), labels)
},
newStore: func() interface{} {
return &gaugeState{
raw: core.NewFloat64Number(0),
}
},
storeCollect: func(store interface{}, value core.Number, ts time.Time) {
gs := store.(*gaugeState)
if !ts.Before(gs.ts) {
gs.ts = ts
gs.raw.SetFloat64Atomic(value.AsFloat64())
}
},
storeExpect: func(store interface{}, value core.Number) {
gs := store.(*gaugeState)
gs.raw.SetFloat64Atomic(value.AsFloat64())
},
readStore: func(store interface{}) core.Number {
gs := store.(*gaugeState)
return gs.raw.AsNumberAtomic()
},
equalValues: float64sEqual,
}
}
func TestStressFloat64GaugeNormal(t *testing.T) {
stressTest(t, floatGaugeTestImpl(false))
}
func TestStressFloat64GaugeMonotonic(t *testing.T) {
stressTest(t, floatGaugeTestImpl(true))
}