github.com/newrelic/go-agent@v3.26.0+incompatible/internal/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  	"runtime"
     8  	"time"
     9  
    10  	"github.com/newrelic/go-agent/internal/logger"
    11  	"github.com/newrelic/go-agent/internal/sysinfo"
    12  )
    13  
    14  // Sample is a system/runtime snapshot.
    15  type Sample struct {
    16  	when         time.Time
    17  	memStats     runtime.MemStats
    18  	usage        sysinfo.Usage
    19  	numGoroutine int
    20  	numCPU       int
    21  }
    22  
    23  func bytesToMebibytesFloat(bts uint64) float64 {
    24  	return float64(bts) / (1024 * 1024)
    25  }
    26  
    27  // GetSample gathers a new Sample.
    28  func GetSample(now time.Time, lg logger.Logger) *Sample {
    29  	s := Sample{
    30  		when:         now,
    31  		numGoroutine: runtime.NumGoroutine(),
    32  		numCPU:       runtime.NumCPU(),
    33  	}
    34  
    35  	if usage, err := sysinfo.GetUsage(); err == nil {
    36  		s.usage = usage
    37  	} else {
    38  		lg.Warn("unable to usage", map[string]interface{}{
    39  			"error": err.Error(),
    40  		})
    41  	}
    42  
    43  	runtime.ReadMemStats(&s.memStats)
    44  
    45  	return &s
    46  }
    47  
    48  type cpuStats struct {
    49  	used     time.Duration
    50  	fraction float64 // used / (elapsed * numCPU)
    51  }
    52  
    53  // Stats contains system information for a period of time.
    54  type Stats struct {
    55  	numGoroutine    int
    56  	allocBytes      uint64
    57  	heapObjects     uint64
    58  	user            cpuStats
    59  	system          cpuStats
    60  	gcPauseFraction float64
    61  	deltaNumGC      uint32
    62  	deltaPauseTotal time.Duration
    63  	minPause        time.Duration
    64  	maxPause        time.Duration
    65  }
    66  
    67  // Samples is used as the parameter to GetStats to avoid mixing up the previous
    68  // and current sample.
    69  type Samples struct {
    70  	Previous *Sample
    71  	Current  *Sample
    72  }
    73  
    74  // GetStats combines two Samples into a Stats.
    75  func GetStats(ss Samples) Stats {
    76  	cur := ss.Current
    77  	prev := ss.Previous
    78  	elapsed := cur.when.Sub(prev.when)
    79  
    80  	s := Stats{
    81  		numGoroutine: cur.numGoroutine,
    82  		allocBytes:   cur.memStats.Alloc,
    83  		heapObjects:  cur.memStats.HeapObjects,
    84  	}
    85  
    86  	// CPU Utilization
    87  	totalCPUSeconds := elapsed.Seconds() * float64(cur.numCPU)
    88  	if prev.usage.User != 0 && cur.usage.User > prev.usage.User {
    89  		s.user.used = cur.usage.User - prev.usage.User
    90  		s.user.fraction = s.user.used.Seconds() / totalCPUSeconds
    91  	}
    92  	if prev.usage.System != 0 && cur.usage.System > prev.usage.System {
    93  		s.system.used = cur.usage.System - prev.usage.System
    94  		s.system.fraction = s.system.used.Seconds() / totalCPUSeconds
    95  	}
    96  
    97  	// GC Pause Fraction
    98  	deltaPauseTotalNs := cur.memStats.PauseTotalNs - prev.memStats.PauseTotalNs
    99  	frac := float64(deltaPauseTotalNs) / float64(elapsed.Nanoseconds())
   100  	s.gcPauseFraction = frac
   101  
   102  	// GC Pauses
   103  	if deltaNumGC := cur.memStats.NumGC - prev.memStats.NumGC; deltaNumGC > 0 {
   104  		// In case more than 256 pauses have happened between samples
   105  		// and we are examining a subset of the pauses, we ensure that
   106  		// the min and max are not on the same side of the average by
   107  		// using the average as the starting min and max.
   108  		maxPauseNs := deltaPauseTotalNs / uint64(deltaNumGC)
   109  		minPauseNs := deltaPauseTotalNs / uint64(deltaNumGC)
   110  		for i := prev.memStats.NumGC + 1; i <= cur.memStats.NumGC; i++ {
   111  			pause := cur.memStats.PauseNs[(i+255)%256]
   112  			if pause > maxPauseNs {
   113  				maxPauseNs = pause
   114  			}
   115  			if pause < minPauseNs {
   116  				minPauseNs = pause
   117  			}
   118  		}
   119  		s.deltaPauseTotal = time.Duration(deltaPauseTotalNs) * time.Nanosecond
   120  		s.deltaNumGC = deltaNumGC
   121  		s.minPause = time.Duration(minPauseNs) * time.Nanosecond
   122  		s.maxPause = time.Duration(maxPauseNs) * time.Nanosecond
   123  	}
   124  
   125  	return s
   126  }
   127  
   128  // MergeIntoHarvest implements Harvestable.
   129  func (s Stats) MergeIntoHarvest(h *Harvest) {
   130  	h.Metrics.addValue(heapObjectsAllocated, "", float64(s.heapObjects), forced)
   131  	h.Metrics.addValue(runGoroutine, "", float64(s.numGoroutine), forced)
   132  	h.Metrics.addValueExclusive(memoryPhysical, "", bytesToMebibytesFloat(s.allocBytes), 0, forced)
   133  	h.Metrics.addValueExclusive(cpuUserUtilization, "", s.user.fraction, 0, forced)
   134  	h.Metrics.addValueExclusive(cpuSystemUtilization, "", s.system.fraction, 0, forced)
   135  	h.Metrics.addValue(cpuUserTime, "", s.user.used.Seconds(), forced)
   136  	h.Metrics.addValue(cpuSystemTime, "", s.system.used.Seconds(), forced)
   137  	h.Metrics.addValueExclusive(gcPauseFraction, "", s.gcPauseFraction, 0, forced)
   138  	if s.deltaNumGC > 0 {
   139  		h.Metrics.add(gcPauses, "", metricData{
   140  			countSatisfied:  float64(s.deltaNumGC),
   141  			totalTolerated:  s.deltaPauseTotal.Seconds(),
   142  			exclusiveFailed: 0,
   143  			min:             s.minPause.Seconds(),
   144  			max:             s.maxPause.Seconds(),
   145  			sumSquares:      s.deltaPauseTotal.Seconds() * s.deltaPauseTotal.Seconds(),
   146  		}, forced)
   147  	}
   148  }