1// Copyright 2018 The Prometheus Authors
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
14package prometheus
15
16import (
17	"runtime"
18	"testing"
19	"time"
20
21	dto "github.com/prometheus/client_model/go"
22)
23
24func TestGoCollectorGoroutines(t *testing.T) {
25	var (
26		c               = NewGoCollector()
27		metricCh        = make(chan Metric)
28		waitCh          = make(chan struct{})
29		endGoroutineCh  = make(chan struct{})
30		endCollectionCh = make(chan struct{})
31		old             = -1
32	)
33	defer func() {
34		close(endGoroutineCh)
35		// Drain the collect channel to prevent goroutine leak.
36		for {
37			select {
38			case <-metricCh:
39			case <-endCollectionCh:
40				return
41			}
42		}
43	}()
44
45	go func() {
46		c.Collect(metricCh)
47		go func(c <-chan struct{}) {
48			<-c
49		}(endGoroutineCh)
50		<-waitCh
51		c.Collect(metricCh)
52		close(endCollectionCh)
53	}()
54
55	for {
56		select {
57		case m := <-metricCh:
58			// m can be Gauge or Counter,
59			// currently just test the go_goroutines Gauge
60			// and ignore others.
61			if m.Desc().fqName != "go_goroutines" {
62				continue
63			}
64			pb := &dto.Metric{}
65			m.Write(pb)
66			if pb.GetGauge() == nil {
67				continue
68			}
69
70			if old == -1 {
71				old = int(pb.GetGauge().GetValue())
72				close(waitCh)
73				continue
74			}
75
76			if diff := int(pb.GetGauge().GetValue()) - old; diff != 1 {
77				// TODO: This is flaky in highly concurrent situations.
78				t.Errorf("want 1 new goroutine, got %d", diff)
79			}
80		case <-time.After(1 * time.Second):
81			t.Fatalf("expected collect timed out")
82		}
83		break
84	}
85}
86
87func TestGoCollectorGC(t *testing.T) {
88	var (
89		c               = NewGoCollector()
90		metricCh        = make(chan Metric)
91		waitCh          = make(chan struct{})
92		endCollectionCh = make(chan struct{})
93		oldGC           uint64
94		oldPause        float64
95	)
96
97	go func() {
98		c.Collect(metricCh)
99		// force GC
100		runtime.GC()
101		<-waitCh
102		c.Collect(metricCh)
103		close(endCollectionCh)
104	}()
105
106	defer func() {
107		// Drain the collect channel to prevent goroutine leak.
108		for {
109			select {
110			case <-metricCh:
111			case <-endCollectionCh:
112				return
113			}
114		}
115	}()
116
117	first := true
118	for {
119		select {
120		case metric := <-metricCh:
121			pb := &dto.Metric{}
122			metric.Write(pb)
123			if pb.GetSummary() == nil {
124				continue
125			}
126			if len(pb.GetSummary().Quantile) != 5 {
127				t.Errorf("expected 4 buckets, got %d", len(pb.GetSummary().Quantile))
128			}
129			for idx, want := range []float64{0.0, 0.25, 0.5, 0.75, 1.0} {
130				if *pb.GetSummary().Quantile[idx].Quantile != want {
131					t.Errorf("bucket #%d is off, got %f, want %f", idx, *pb.GetSummary().Quantile[idx].Quantile, want)
132				}
133			}
134			if first {
135				first = false
136				oldGC = *pb.GetSummary().SampleCount
137				oldPause = *pb.GetSummary().SampleSum
138				close(waitCh)
139				continue
140			}
141			if diff := *pb.GetSummary().SampleCount - oldGC; diff < 1 {
142				t.Errorf("want at least 1 new garbage collection run, got %d", diff)
143			}
144			if diff := *pb.GetSummary().SampleSum - oldPause; diff <= 0 {
145				t.Errorf("want an increase in pause time, got a change of %f", diff)
146			}
147		case <-time.After(1 * time.Second):
148			t.Fatalf("expected collect timed out")
149		}
150		break
151	}
152}
153
154func TestGoCollectorMemStats(t *testing.T) {
155	var (
156		c   = NewGoCollector().(*goCollector)
157		got uint64
158	)
159
160	checkCollect := func(want uint64) {
161		metricCh := make(chan Metric)
162		endCh := make(chan struct{})
163
164		go func() {
165			c.Collect(metricCh)
166			close(endCh)
167		}()
168	Collect:
169		for {
170			select {
171			case metric := <-metricCh:
172				if metric.Desc().fqName != "go_memstats_alloc_bytes" {
173					continue Collect
174				}
175				pb := &dto.Metric{}
176				metric.Write(pb)
177				got = uint64(pb.GetGauge().GetValue())
178			case <-endCh:
179				break Collect
180			}
181		}
182		if want != got {
183			t.Errorf("unexpected value of go_memstats_alloc_bytes, want %d, got %d", want, got)
184		}
185	}
186
187	// Speed up the timing to make the test faster.
188	c.msMaxWait = 5 * time.Millisecond
189	c.msMaxAge = 50 * time.Millisecond
190
191	// Scenario 1: msRead responds slowly, no previous memstats available,
192	// msRead is executed anyway.
193	c.msRead = func(ms *runtime.MemStats) {
194		time.Sleep(20 * time.Millisecond)
195		ms.Alloc = 1
196	}
197	checkCollect(1)
198	// Now msLast is set.
199	c.msMtx.Lock()
200	if want, got := uint64(1), c.msLast.Alloc; want != got {
201		t.Errorf("unexpected of msLast.Alloc, want %d, got %d", want, got)
202	}
203	c.msMtx.Unlock()
204
205	// Scenario 2: msRead responds fast, previous memstats available, new
206	// value collected.
207	c.msRead = func(ms *runtime.MemStats) {
208		ms.Alloc = 2
209	}
210	checkCollect(2)
211	// msLast is set, too.
212	c.msMtx.Lock()
213	if want, got := uint64(2), c.msLast.Alloc; want != got {
214		t.Errorf("unexpected of msLast.Alloc, want %d, got %d", want, got)
215	}
216	c.msMtx.Unlock()
217
218	// Scenario 3: msRead responds slowly, previous memstats available, old
219	// value collected.
220	c.msRead = func(ms *runtime.MemStats) {
221		time.Sleep(20 * time.Millisecond)
222		ms.Alloc = 3
223	}
224	checkCollect(2)
225	// After waiting, new value is still set in msLast.
226	time.Sleep(80 * time.Millisecond)
227	c.msMtx.Lock()
228	if want, got := uint64(3), c.msLast.Alloc; want != got {
229		t.Errorf("unexpected of msLast.Alloc, want %d, got %d", want, got)
230	}
231	c.msMtx.Unlock()
232
233	// Scenario 4: msRead responds slowly, previous memstats is too old, new
234	// value collected.
235	c.msRead = func(ms *runtime.MemStats) {
236		time.Sleep(20 * time.Millisecond)
237		ms.Alloc = 4
238	}
239	checkCollect(4)
240	c.msMtx.Lock()
241	if want, got := uint64(4), c.msLast.Alloc; want != got {
242		t.Errorf("unexpected of msLast.Alloc, want %d, got %d", want, got)
243	}
244	c.msMtx.Unlock()
245}
246