mirror of
https://github.com/open-telemetry/opentelemetry-go.git
synced 2025-04-11 11:21:59 +02:00
Refactor metric records logic (#468)
* Refactor metric records logic. Signed-off-by: Bogdan Cristian Drutu <bogdandrutu@gmail.com> * Fix lint errors Signed-off-by: Bogdan Cristian Drutu <bogdandrutu@gmail.com> * Fix a bug that we try to readd the old entry instead of a new one. Signed-off-by: Bogdan Cristian Drutu <bogdandrutu@gmail.com> * Update comments in refcount_mapped. Signed-off-by: Bogdan Cristian Drutu <bogdandrutu@gmail.com> * Remove the need to use a records list, iterate over the map. Signed-off-by: Bogdan Cristian Drutu <bogdandrutu@gmail.com> * Fix comments and typos Signed-off-by: Bogdan Cristian Drutu <bogdandrutu@gmail.com> * Fix more comments Signed-off-by: Bogdan Cristian Drutu <bogdandrutu@gmail.com> * Clarify tryUnmap comment Signed-off-by: Bogdan Cristian Drutu <bogdandrutu@gmail.com> * Fix one more typo. Signed-off-by: Bogdan Cristian Drutu <bogdandrutu@gmail.com>
This commit is contained in:
parent
4818358f94
commit
69df67d449
@ -12,20 +12,12 @@ import (
|
||||
func TestMain(m *testing.M) {
|
||||
fields := []ottest.FieldOffset{
|
||||
{
|
||||
Name: "record.refcount",
|
||||
Offset: unsafe.Offsetof(record{}.refcount),
|
||||
Name: "record.refMapped.value",
|
||||
Offset: unsafe.Offsetof(record{}.refMapped.value),
|
||||
},
|
||||
{
|
||||
Name: "record.collectedEpoch",
|
||||
Offset: unsafe.Offsetof(record{}.collectedEpoch),
|
||||
},
|
||||
{
|
||||
Name: "record.modifiedEpoch",
|
||||
Offset: unsafe.Offsetof(record{}.modifiedEpoch),
|
||||
},
|
||||
{
|
||||
Name: "record.reclaim",
|
||||
Offset: unsafe.Offsetof(record{}.reclaim),
|
||||
Name: "record.modified",
|
||||
Offset: unsafe.Offsetof(record{}.modified),
|
||||
},
|
||||
}
|
||||
if !ottest.Aligned8Byte(fields, os.Stderr) {
|
||||
|
@ -14,11 +14,6 @@
|
||||
|
||||
package metric
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func (l *sortedLabels) Len() int {
|
||||
return len(*l)
|
||||
}
|
||||
@ -30,51 +25,3 @@ func (l *sortedLabels) Swap(i, j int) {
|
||||
func (l *sortedLabels) Less(i, j int) bool {
|
||||
return (*l)[i].Key < (*l)[j].Key
|
||||
}
|
||||
|
||||
func (m *SDK) addPrimary(rec *record) {
|
||||
for {
|
||||
rec.next.primary.store(m.records.primary.load())
|
||||
if atomic.CompareAndSwapPointer(
|
||||
&m.records.primary.ptr,
|
||||
rec.next.primary.ptr,
|
||||
unsafe.Pointer(rec),
|
||||
) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *SDK) addReclaim(rec *record) {
|
||||
for {
|
||||
rec.next.reclaim.store(m.records.reclaim.load())
|
||||
if atomic.CompareAndSwapPointer(
|
||||
&m.records.reclaim.ptr,
|
||||
rec.next.reclaim.ptr,
|
||||
unsafe.Pointer(rec),
|
||||
) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *singlePtr) swapNil() *record {
|
||||
for {
|
||||
newValue := unsafe.Pointer(nil)
|
||||
swapped := atomic.LoadPointer(&s.ptr)
|
||||
if atomic.CompareAndSwapPointer(&s.ptr, swapped, newValue) {
|
||||
return (*record)(swapped)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *singlePtr) load() *record {
|
||||
return (*record)(atomic.LoadPointer(&s.ptr))
|
||||
}
|
||||
|
||||
func (s *singlePtr) store(r *record) {
|
||||
atomic.StorePointer(&s.ptr, unsafe.Pointer(r))
|
||||
}
|
||||
|
||||
func (s *singlePtr) clear() {
|
||||
atomic.StorePointer(&s.ptr, unsafe.Pointer(nil))
|
||||
}
|
||||
|
65
sdk/metric/refcount_mapped.go
Normal file
65
sdk/metric/refcount_mapped.go
Normal file
@ -0,0 +1,65 @@
|
||||
// Copyright 2020, 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.
|
||||
|
||||
package metric
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// refcountMapped atomically counts the number of references (usages) of an entry
|
||||
// while also keeping a state of mapped/unmapped into a different data structure
|
||||
// (an external map or list for example).
|
||||
//
|
||||
// refcountMapped uses an atomic value where the least significant bit is used to
|
||||
// keep the state of mapping ('1' is used for unmapped and '0' is for mapped) and
|
||||
// the rest of the bits are used for refcounting.
|
||||
type refcountMapped struct {
|
||||
// refcount has to be aligned for 64-bit atomic operations.
|
||||
value int64
|
||||
}
|
||||
|
||||
// ref returns true if the entry is still mapped and increases the
|
||||
// reference usages, if unmapped returns false.
|
||||
func (rm *refcountMapped) ref() bool {
|
||||
// Check if this entry was marked as unmapped between the moment
|
||||
// we got a reference to it (or will be removed very soon) and here.
|
||||
return atomic.AddInt64(&rm.value, 2)&1 == 0
|
||||
}
|
||||
|
||||
func (rm *refcountMapped) unref() {
|
||||
atomic.AddInt64(&rm.value, -2)
|
||||
}
|
||||
|
||||
// inUse returns true if there is a reference to the entry and it is mapped.
|
||||
func (rm *refcountMapped) inUse() bool {
|
||||
val := atomic.LoadInt64(&rm.value)
|
||||
return val >= 2 && val&1 == 0
|
||||
}
|
||||
|
||||
// tryUnmap flips the mapped bit to "unmapped" state and returns true if both of the
|
||||
// following conditions are true upon entry to this function:
|
||||
// * There are no active references;
|
||||
// * The mapped bit is in "mapped" state.
|
||||
// Otherwise no changes are done to mapped bit and false is returned.
|
||||
func (rm *refcountMapped) tryUnmap() bool {
|
||||
if atomic.LoadInt64(&rm.value) != 0 {
|
||||
return false
|
||||
}
|
||||
return atomic.CompareAndSwapInt64(
|
||||
&rm.value,
|
||||
0,
|
||||
1,
|
||||
)
|
||||
}
|
@ -21,7 +21,6 @@ import (
|
||||
"sort"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"unsafe"
|
||||
|
||||
"go.opentelemetry.io/otel/api/core"
|
||||
"go.opentelemetry.io/otel/api/metric"
|
||||
@ -47,10 +46,6 @@ type (
|
||||
// w/ zero arguments.
|
||||
empty labels
|
||||
|
||||
// records is the head of both the primary and the
|
||||
// reclaim records lists.
|
||||
records doublePtr
|
||||
|
||||
// currentEpoch is the current epoch number. It is
|
||||
// incremented in `Collect()`.
|
||||
currentEpoch int64
|
||||
@ -97,32 +92,15 @@ type (
|
||||
// `record` in existence at a time, although at most one can
|
||||
// be referenced from the `SDK.current` map.
|
||||
record struct {
|
||||
// refcount counts the number of active handles on
|
||||
// referring to this record. active handles prevent
|
||||
// removing the record from the current map.
|
||||
//
|
||||
// refcount has to be aligned for 64-bit atomic operations.
|
||||
refcount int64
|
||||
// refMapped keeps track of refcounts and the mapping state to the
|
||||
// SDK.current map.
|
||||
refMapped refcountMapped
|
||||
|
||||
// collectedEpoch is the epoch number for which this
|
||||
// record has been exported. This is modified by the
|
||||
// `Collect()` method.
|
||||
// modified is an atomic boolean that tracks if the current record
|
||||
// was modified since the last Collect().
|
||||
//
|
||||
// collectedEpoch has to be aligned for 64-bit atomic operations.
|
||||
collectedEpoch int64
|
||||
|
||||
// modifiedEpoch is the latest epoch number for which
|
||||
// this record was updated. Generally, if
|
||||
// modifiedEpoch is less than collectedEpoch, this
|
||||
// record is due for reclaimation.
|
||||
//
|
||||
// modifiedEpoch has to be aligned for 64-bit atomic operations.
|
||||
modifiedEpoch int64
|
||||
|
||||
// reclaim is an atomic to control the start of reclaiming.
|
||||
//
|
||||
// reclaim has to be aligned for 64-bit atomic operations.
|
||||
reclaim int64
|
||||
// modified has to be aligned for 64-bit atomic operations.
|
||||
modified int64
|
||||
|
||||
// labels is the LabelSet passed by the user.
|
||||
labels *labels
|
||||
@ -134,25 +112,9 @@ type (
|
||||
// depending on the type of aggregation. If nil, the
|
||||
// metric was disabled by the exporter.
|
||||
recorder export.Aggregator
|
||||
|
||||
// next contains the next pointer for both the primary
|
||||
// and the reclaim lists.
|
||||
next doublePtr
|
||||
}
|
||||
|
||||
ErrorHandler func(error)
|
||||
|
||||
// singlePointer wraps an unsafe.Pointer and supports basic
|
||||
// load(), store(), clear(), and swapNil() operations.
|
||||
singlePtr struct {
|
||||
ptr unsafe.Pointer
|
||||
}
|
||||
|
||||
// doublePtr is used for the head and next links of two lists.
|
||||
doublePtr struct {
|
||||
primary singlePtr
|
||||
reclaim singlePtr
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
@ -160,12 +122,6 @@ var (
|
||||
_ api.LabelSet = &labels{}
|
||||
_ api.InstrumentImpl = &instrument{}
|
||||
_ api.BoundInstrumentImpl = &record{}
|
||||
|
||||
// hazardRecord is used as a pointer value that indicates the
|
||||
// value is not included in any list. (`nil` would be
|
||||
// ambiguous, since the final element in a list has `nil` as
|
||||
// the next pointer).
|
||||
hazardRecord = &record{}
|
||||
)
|
||||
|
||||
func (i *instrument) Meter() api.Meter {
|
||||
@ -186,31 +142,48 @@ func (i *instrument) acquireHandle(ls *labels) *record {
|
||||
if actual, ok := i.meter.current.Load(mk); ok {
|
||||
// Existing record case, only one allocation so far.
|
||||
rec := actual.(*record)
|
||||
atomic.AddInt64(&rec.refcount, 1)
|
||||
return rec
|
||||
if rec.refMapped.ref() {
|
||||
// At this moment it is guaranteed that the entry is in
|
||||
// the map and will not be removed.
|
||||
return rec
|
||||
}
|
||||
// This entry is no longer mapped, try to add a new entry.
|
||||
}
|
||||
|
||||
// There's a memory allocation here.
|
||||
rec := &record{
|
||||
labels: ls,
|
||||
descriptor: i.descriptor,
|
||||
refcount: 1,
|
||||
collectedEpoch: -1,
|
||||
modifiedEpoch: 0,
|
||||
recorder: i.meter.batcher.AggregatorFor(i.descriptor),
|
||||
labels: ls,
|
||||
descriptor: i.descriptor,
|
||||
refMapped: refcountMapped{value: 2},
|
||||
modified: 0,
|
||||
recorder: i.meter.batcher.AggregatorFor(i.descriptor),
|
||||
}
|
||||
|
||||
// Load/Store: there's a memory allocation to place `mk` into
|
||||
// an interface here.
|
||||
if actual, loaded := i.meter.current.LoadOrStore(mk, rec); loaded {
|
||||
// Existing record case.
|
||||
rec = actual.(*record)
|
||||
atomic.AddInt64(&rec.refcount, 1)
|
||||
for {
|
||||
// Load/Store: there's a memory allocation to place `mk` into
|
||||
// an interface here.
|
||||
if actual, loaded := i.meter.current.LoadOrStore(mk, rec); loaded {
|
||||
// Existing record case. Cannot change rec here because if fail
|
||||
// will try to add rec again to avoid new allocations.
|
||||
oldRec := actual.(*record)
|
||||
if oldRec.refMapped.ref() {
|
||||
// At this moment it is guaranteed that the entry is in
|
||||
// the map and will not be removed.
|
||||
return oldRec
|
||||
}
|
||||
// This loaded entry is marked as unmapped (so Collect will remove
|
||||
// it from the map immediately), try again - this is a busy waiting
|
||||
// strategy to wait until Collect() removes this entry from the map.
|
||||
//
|
||||
// This can be improved by having a list of "Unmapped" entries for
|
||||
// one time only usages, OR we can make this a blocking path and use
|
||||
// a Mutex that protects the delete operation (delete only if the old
|
||||
// record is associated with the key).
|
||||
continue
|
||||
}
|
||||
// The new entry was added to the map, good to go.
|
||||
return rec
|
||||
}
|
||||
|
||||
i.meter.addPrimary(rec)
|
||||
return rec
|
||||
}
|
||||
|
||||
func (i *instrument) Bind(ls api.LabelSet) api.BoundInstrumentImpl {
|
||||
@ -355,22 +328,6 @@ func (m *SDK) NewFloat64Measure(name string, mos ...api.MeasureOptionApplier) ap
|
||||
return api.WrapFloat64MeasureInstrument(m.newMeasureInstrument(name, core.Float64NumberKind, mos...))
|
||||
}
|
||||
|
||||
// saveFromReclaim puts a record onto the "reclaim" list when it
|
||||
// detects an attempt to delete the record while it is still in use.
|
||||
func (m *SDK) saveFromReclaim(rec *record) {
|
||||
for {
|
||||
reclaimed := atomic.LoadInt64(&rec.reclaim)
|
||||
if reclaimed != 0 {
|
||||
return
|
||||
}
|
||||
if atomic.CompareAndSwapInt64(&rec.reclaim, 0, 1) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
m.addReclaim(rec)
|
||||
}
|
||||
|
||||
// Collect traverses the list of active records and exports data for
|
||||
// each active instrument. Collect() may not be called concurrently.
|
||||
//
|
||||
@ -384,45 +341,25 @@ func (m *SDK) Collect(ctx context.Context) int {
|
||||
|
||||
checkpointed := 0
|
||||
|
||||
var next *record
|
||||
for inuse := m.records.primary.swapNil(); inuse != nil; inuse = next {
|
||||
next = inuse.next.primary.load()
|
||||
m.current.Range(func(key interface{}, value interface{}) bool {
|
||||
inuse := value.(*record)
|
||||
unmapped := inuse.refMapped.tryUnmap()
|
||||
// If able to unmap then remove the record from the current Map.
|
||||
if unmapped {
|
||||
m.current.Delete(inuse.mapkey())
|
||||
}
|
||||
|
||||
refcount := atomic.LoadInt64(&inuse.refcount)
|
||||
|
||||
if refcount > 0 {
|
||||
// Always report the values if a reference to the Record is active,
|
||||
// this is to keep the previous behavior.
|
||||
// TODO: Reconsider this logic.
|
||||
if inuse.refMapped.inUse() || atomic.LoadInt64(&inuse.modified) != 0 {
|
||||
atomic.StoreInt64(&inuse.modified, 0)
|
||||
checkpointed += m.checkpoint(ctx, inuse)
|
||||
m.addPrimary(inuse)
|
||||
continue
|
||||
}
|
||||
|
||||
modified := atomic.LoadInt64(&inuse.modifiedEpoch)
|
||||
collected := atomic.LoadInt64(&inuse.collectedEpoch)
|
||||
checkpointed += m.checkpoint(ctx, inuse)
|
||||
|
||||
if modified >= collected {
|
||||
atomic.StoreInt64(&inuse.collectedEpoch, m.currentEpoch)
|
||||
m.addPrimary(inuse)
|
||||
continue
|
||||
}
|
||||
|
||||
// Remove this entry.
|
||||
m.current.Delete(inuse.mapkey())
|
||||
inuse.next.primary.store(hazardRecord)
|
||||
}
|
||||
|
||||
for chances := m.records.reclaim.swapNil(); chances != nil; chances = next {
|
||||
atomic.StoreInt64(&chances.collectedEpoch, m.currentEpoch)
|
||||
|
||||
next = chances.next.reclaim.load()
|
||||
chances.next.reclaim.clear()
|
||||
atomic.StoreInt64(&chances.reclaim, 0)
|
||||
|
||||
if chances.next.primary.load() == hazardRecord {
|
||||
checkpointed += m.checkpoint(ctx, chances)
|
||||
m.addPrimary(chances)
|
||||
}
|
||||
}
|
||||
// Always continue to iterate over the entire map.
|
||||
return true
|
||||
})
|
||||
|
||||
m.currentEpoch++
|
||||
return checkpointed
|
||||
@ -474,29 +411,11 @@ func (r *record) RecordOne(ctx context.Context, number core.Number) {
|
||||
}
|
||||
|
||||
func (r *record) Unbind() {
|
||||
for {
|
||||
collected := atomic.LoadInt64(&r.collectedEpoch)
|
||||
modified := atomic.LoadInt64(&r.modifiedEpoch)
|
||||
|
||||
updated := collected + 1
|
||||
|
||||
if modified == updated {
|
||||
// No change
|
||||
break
|
||||
}
|
||||
if !atomic.CompareAndSwapInt64(&r.modifiedEpoch, modified, updated) {
|
||||
continue
|
||||
}
|
||||
|
||||
if modified < collected {
|
||||
// This record could have been reclaimed.
|
||||
r.labels.meter.saveFromReclaim(r)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
_ = atomic.AddInt64(&r.refcount, -1)
|
||||
// Record was modified, inform the Collect() that things need to be collected.
|
||||
// TODO: Reconsider if we should marked as modified when an Update happens and
|
||||
// collect only when updates happened even for Bounds.
|
||||
atomic.StoreInt64(&r.modified, 1)
|
||||
r.refMapped.unref()
|
||||
}
|
||||
|
||||
func (r *record) mapkey() mapkey {
|
||||
|
Loading…
x
Reference in New Issue
Block a user