github.com/newrelic/go-agent@v3.26.0+incompatible/internal/adaptive_sampler.go (about)

     1  // Copyright 2020 New Relic Corporation. All rights reserved.
     2  // SPDX-License-Identifier: Apache-2.0
     3  
     4  package internal
     5  
     6  import (
     7  	"math"
     8  	"sync"
     9  	"time"
    10  )
    11  
    12  // AdaptiveSampler calculates which transactions should be sampled.  An interface
    13  // is used in the connect reply to facilitate testing.
    14  type AdaptiveSampler interface {
    15  	ComputeSampled(priority float32, now time.Time) bool
    16  }
    17  
    18  // SampleEverything is used for testing.
    19  type SampleEverything struct{}
    20  
    21  // SampleNothing is used when the application is not yet connected.
    22  type SampleNothing struct{}
    23  
    24  // ComputeSampled implements AdaptiveSampler.
    25  func (s SampleEverything) ComputeSampled(priority float32, now time.Time) bool { return true }
    26  
    27  // ComputeSampled implements AdaptiveSampler.
    28  func (s SampleNothing) ComputeSampled(priority float32, now time.Time) bool { return false }
    29  
    30  type adaptiveSampler struct {
    31  	sync.Mutex
    32  	period time.Duration
    33  	target uint64
    34  
    35  	// Transactions with priority higher than this are sampled.
    36  	// This is 1 - sampleRatio.
    37  	priorityMin float32
    38  
    39  	currentPeriod struct {
    40  		numSampled uint64
    41  		numSeen    uint64
    42  		end        time.Time
    43  	}
    44  }
    45  
    46  // NewAdaptiveSampler creates an AdaptiveSampler.
    47  func NewAdaptiveSampler(period time.Duration, target uint64, now time.Time) AdaptiveSampler {
    48  	as := &adaptiveSampler{}
    49  	as.period = period
    50  	as.target = target
    51  	as.currentPeriod.end = now.Add(period)
    52  
    53  	// Sample the first transactions in the first period.
    54  	as.priorityMin = 0.0
    55  	return as
    56  }
    57  
    58  // ComputeSampled calculates if the transaction should be sampled.
    59  func (as *adaptiveSampler) ComputeSampled(priority float32, now time.Time) bool {
    60  	as.Lock()
    61  	defer as.Unlock()
    62  
    63  	// If the current time is after the end of the "currentPeriod".  This is in
    64  	// a `for`/`while` loop in case there's a harvest where no sampling happened.
    65  	// i.e. for situations where a single call to
    66  	//    as.currentPeriod.end = as.currentPeriod.end.Add(as.period)
    67  	// might not catch us up to the current period
    68  	for now.After(as.currentPeriod.end) {
    69  		as.priorityMin = 0.0
    70  		if as.currentPeriod.numSeen > 0 {
    71  			sampledRatio := float32(as.target) / float32(as.currentPeriod.numSeen)
    72  			as.priorityMin = 1.0 - sampledRatio
    73  		}
    74  		as.currentPeriod.numSampled = 0
    75  		as.currentPeriod.numSeen = 0
    76  		as.currentPeriod.end = as.currentPeriod.end.Add(as.period)
    77  	}
    78  
    79  	as.currentPeriod.numSeen++
    80  
    81  	// exponential backoff -- if the number of sampled items is greater than our
    82  	// target, we need to apply the exponential backoff
    83  	if as.currentPeriod.numSampled > as.target {
    84  		if as.computeSampledBackoff(as.target, as.currentPeriod.numSeen, as.currentPeriod.numSampled) {
    85  			as.currentPeriod.numSampled++
    86  			return true
    87  		}
    88  		return false
    89  	}
    90  
    91  	if priority >= as.priorityMin {
    92  		as.currentPeriod.numSampled++
    93  		return true
    94  	}
    95  
    96  	return false
    97  }
    98  
    99  func (as *adaptiveSampler) computeSampledBackoff(target uint64, decidedCount uint64, sampledTrueCount uint64) bool {
   100  	return float64(RandUint64N(decidedCount)) <
   101  		math.Pow(float64(target), (float64(target)/float64(sampledTrueCount)))-math.Pow(float64(target), 0.5)
   102  }