File: 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 (148 lines) | stat: -rw-r--r-- 4,470 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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
// Copyright 2020 New Relic Corporation. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package internal

import (
	"runtime"
	"time"

	"github.com/newrelic/go-agent/internal/logger"
	"github.com/newrelic/go-agent/internal/sysinfo"
)

// Sample is a system/runtime snapshot.
type Sample struct {
	when         time.Time
	memStats     runtime.MemStats
	usage        sysinfo.Usage
	numGoroutine int
	numCPU       int
}

func bytesToMebibytesFloat(bts uint64) float64 {
	return float64(bts) / (1024 * 1024)
}

// GetSample gathers a new Sample.
func GetSample(now time.Time, lg logger.Logger) *Sample {
	s := Sample{
		when:         now,
		numGoroutine: runtime.NumGoroutine(),
		numCPU:       runtime.NumCPU(),
	}

	if usage, err := sysinfo.GetUsage(); err == nil {
		s.usage = usage
	} else {
		lg.Warn("unable to usage", map[string]interface{}{
			"error": err.Error(),
		})
	}

	runtime.ReadMemStats(&s.memStats)

	return &s
}

type cpuStats struct {
	used     time.Duration
	fraction float64 // used / (elapsed * numCPU)
}

// Stats contains system information for a period of time.
type Stats struct {
	numGoroutine    int
	allocBytes      uint64
	heapObjects     uint64
	user            cpuStats
	system          cpuStats
	gcPauseFraction float64
	deltaNumGC      uint32
	deltaPauseTotal time.Duration
	minPause        time.Duration
	maxPause        time.Duration
}

// Samples is used as the parameter to GetStats to avoid mixing up the previous
// and current sample.
type Samples struct {
	Previous *Sample
	Current  *Sample
}

// GetStats combines two Samples into a Stats.
func GetStats(ss Samples) Stats {
	cur := ss.Current
	prev := ss.Previous
	elapsed := cur.when.Sub(prev.when)

	s := Stats{
		numGoroutine: cur.numGoroutine,
		allocBytes:   cur.memStats.Alloc,
		heapObjects:  cur.memStats.HeapObjects,
	}

	// CPU Utilization
	totalCPUSeconds := elapsed.Seconds() * float64(cur.numCPU)
	if prev.usage.User != 0 && cur.usage.User > prev.usage.User {
		s.user.used = cur.usage.User - prev.usage.User
		s.user.fraction = s.user.used.Seconds() / totalCPUSeconds
	}
	if prev.usage.System != 0 && cur.usage.System > prev.usage.System {
		s.system.used = cur.usage.System - prev.usage.System
		s.system.fraction = s.system.used.Seconds() / totalCPUSeconds
	}

	// GC Pause Fraction
	deltaPauseTotalNs := cur.memStats.PauseTotalNs - prev.memStats.PauseTotalNs
	frac := float64(deltaPauseTotalNs) / float64(elapsed.Nanoseconds())
	s.gcPauseFraction = frac

	// GC Pauses
	if deltaNumGC := cur.memStats.NumGC - prev.memStats.NumGC; deltaNumGC > 0 {
		// In case more than 256 pauses have happened between samples
		// and we are examining a subset of the pauses, we ensure that
		// the min and max are not on the same side of the average by
		// using the average as the starting min and max.
		maxPauseNs := deltaPauseTotalNs / uint64(deltaNumGC)
		minPauseNs := deltaPauseTotalNs / uint64(deltaNumGC)
		for i := prev.memStats.NumGC + 1; i <= cur.memStats.NumGC; i++ {
			pause := cur.memStats.PauseNs[(i+255)%256]
			if pause > maxPauseNs {
				maxPauseNs = pause
			}
			if pause < minPauseNs {
				minPauseNs = pause
			}
		}
		s.deltaPauseTotal = time.Duration(deltaPauseTotalNs) * time.Nanosecond
		s.deltaNumGC = deltaNumGC
		s.minPause = time.Duration(minPauseNs) * time.Nanosecond
		s.maxPause = time.Duration(maxPauseNs) * time.Nanosecond
	}

	return s
}

// MergeIntoHarvest implements Harvestable.
func (s Stats) MergeIntoHarvest(h *Harvest) {
	h.Metrics.addValue(heapObjectsAllocated, "", float64(s.heapObjects), forced)
	h.Metrics.addValue(runGoroutine, "", float64(s.numGoroutine), forced)
	h.Metrics.addValueExclusive(memoryPhysical, "", bytesToMebibytesFloat(s.allocBytes), 0, forced)
	h.Metrics.addValueExclusive(cpuUserUtilization, "", s.user.fraction, 0, forced)
	h.Metrics.addValueExclusive(cpuSystemUtilization, "", s.system.fraction, 0, forced)
	h.Metrics.addValue(cpuUserTime, "", s.user.used.Seconds(), forced)
	h.Metrics.addValue(cpuSystemTime, "", s.system.used.Seconds(), forced)
	h.Metrics.addValueExclusive(gcPauseFraction, "", s.gcPauseFraction, 0, forced)
	if s.deltaNumGC > 0 {
		h.Metrics.add(gcPauses, "", metricData{
			countSatisfied:  float64(s.deltaNumGC),
			totalTolerated:  s.deltaPauseTotal.Seconds(),
			exclusiveFailed: 0,
			min:             s.minPause.Seconds(),
			max:             s.maxPause.Seconds(),
			sumSquares:      s.deltaPauseTotal.Seconds() * s.deltaPauseTotal.Seconds(),
		}, forced)
	}
}