File: adaptive_sampler.go

package info (click to toggle)
golang-github-newrelic-go-agent 3.15.2-9
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 8,356 kB
  • sloc: sh: 65; makefile: 6
file content (91 lines) | stat: -rw-r--r-- 2,604 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
// Copyright 2020 New Relic Corporation. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package newrelic

import (
	"math"
	"sync"
	"time"
)

type adaptiveSampler struct {
	sync.Mutex
	period time.Duration
	target uint64

	// Transactions with priority higher than this are sampled.
	// This is 1 - sampleRatio.
	priorityMin float32

	currentPeriod struct {
		numSampled uint64
		numSeen    uint64
		end        time.Time
	}
}

// newAdaptiveSampler creates an adaptiveSampler.
func newAdaptiveSampler(period time.Duration, target uint64, now time.Time) *adaptiveSampler {
	as := &adaptiveSampler{}
	as.period = period
	as.target = target
	as.currentPeriod.end = now.Add(period)

	// Sample the first transactions in the first period.
	as.priorityMin = 0.0
	return as
}

// computeSampled calculates if the transaction should be sampled.
func (as *adaptiveSampler) computeSampled(priority float32, now time.Time) bool {
	as.Lock()
	defer as.Unlock()

	// Never sample anything if the target is zero.  This is not an expected
	// connect reply response, but it is used for the placeholder run (app
	// not connected yet), and is used for testing.
	if 0 == as.target {
		return false
	}

	// If the current time is after the end of the "currentPeriod".  This is in
	// a `for`/`while` loop in case there's a harvest where no sampling happened.
	// i.e. for situations where a single call to
	//    as.currentPeriod.end = as.currentPeriod.end.Add(as.period)
	// might not catch us up to the current period
	for now.After(as.currentPeriod.end) {
		as.priorityMin = 0.0
		if as.currentPeriod.numSeen > 0 {
			sampledRatio := float32(as.target) / float32(as.currentPeriod.numSeen)
			as.priorityMin = 1.0 - sampledRatio
		}
		as.currentPeriod.numSampled = 0
		as.currentPeriod.numSeen = 0
		as.currentPeriod.end = as.currentPeriod.end.Add(as.period)
	}

	as.currentPeriod.numSeen++

	// exponential backoff -- if the number of sampled items is greater than our
	// target, we need to apply the exponential backoff
	if as.currentPeriod.numSampled > as.target {
		if as.computeSampledBackoff(as.target, as.currentPeriod.numSeen, as.currentPeriod.numSampled) {
			as.currentPeriod.numSampled++
			return true
		}
		return false
	}

	if priority >= as.priorityMin {
		as.currentPeriod.numSampled++
		return true
	}

	return false
}

func (as *adaptiveSampler) computeSampledBackoff(target uint64, decidedCount uint64, sampledTrueCount uint64) bool {
	return float64(randUint64N(decidedCount)) <
		math.Pow(float64(target), (float64(target)/float64(sampledTrueCount)))-math.Pow(float64(target), 0.5)
}