metrics_test.mx raw
1 package metrics
2
3 import (
4 "bytes"
5 "testing"
6 )
7
8 // mxHas is a local substring scan. bytes.Contains routes a long haystack
9 // through bytes.Index's Rabin-Karp fallback, which returns -1 for a needle
10 // that is demonstrably present (see the reported stdlib defect), so the tests
11 // cannot use it. This helper is the two-line brute force instead.
12 func mxHas(hay, needle []byte) (ok bool) {
13 for i := 0; i+len(needle) <= len(hay); i++ {
14 if bytes.Equal(hay[i:i+len(needle)], needle) {
15 return true
16 }
17 }
18 return false
19 }
20
21 // TestBucketOf pins the power-of-two bucket map, including the two boundary
22 // cases: values below 2 land in bucket 0, and everything at or above 2^24 is
23 // clamped into the last bucket.
24 func TestBucketOf(t *testing.T) {
25 cases := []struct {
26 ns uint64
27 want int32
28 }{
29 {0, 0},
30 {1, 0},
31 {2, 1},
32 {3, 1},
33 {4, 2},
34 {7, 2},
35 {8, 3},
36 {15, 3},
37 {16, 4},
38 {8388608, 23}, // 2^23
39 {16777216, 24}, // 2^24, first clamped value
40 {1099511627776, 24}, // 2^40, still clamped
41 }
42 for _, c := range cases {
43 got := bucketOf(c.ns)
44 if got != c.want {
45 t.Fatalf("bucketOf(%d) = %d, want %d", c.ns, got, c.want)
46 }
47 }
48 }
49
50 // TestObserveCountsSumAndAverage pins Count / SumNs / AvgNs and the per-bucket
51 // tallies for observations in different buckets.
52 func TestObserveCountsSumAndAverage(t *testing.T) {
53 h := NewHistogram("t")
54 if h.Name != "t" {
55 t.Fatalf("NewHistogram name = %q", h.Name)
56 }
57 h.Observe(0)
58 h.Observe(100)
59 if h.Count != 2 {
60 t.Fatalf("Count = %d, want 2", h.Count)
61 }
62 if h.SumNs != 100 {
63 t.Fatalf("SumNs = %d, want 100", h.SumNs)
64 }
65 if h.AvgNs() != 50 {
66 t.Fatalf("AvgNs = %d, want 50", h.AvgNs())
67 }
68 if h.Buckets[0] != 1 {
69 t.Fatalf("bucket 0 = %d, want 1", h.Buckets[0])
70 }
71 if bucketOf(100) != 6 {
72 t.Fatalf("test assumption broken: bucketOf(100) = %d", bucketOf(100))
73 }
74 if h.Buckets[6] != 1 {
75 t.Fatalf("bucket 6 = %d, want 1", h.Buckets[6])
76 }
77 var total uint64
78 for i := 0; i < numBuckets; i++ {
79 total += h.Buckets[i]
80 }
81 if total != 2 {
82 t.Fatalf("bucket total = %d, want 2", total)
83 }
84 }
85
86 // TestObserveClampsNegative pins that a negative duration is recorded as zero
87 // rather than wrapping to a huge uint64 (which would poison SumNs).
88 func TestObserveClampsNegative(t *testing.T) {
89 h := NewHistogram("neg")
90 h.Observe(-5)
91 if h.Count != 1 {
92 t.Fatalf("Count = %d, want 1", h.Count)
93 }
94 if h.SumNs != 0 {
95 t.Fatalf("SumNs = %d, want 0", h.SumNs)
96 }
97 if h.Buckets[0] != 1 {
98 t.Fatalf("bucket 0 = %d, want 1", h.Buckets[0])
99 }
100 if h.AvgNs() != 0 {
101 t.Fatalf("AvgNs = %d, want 0", h.AvgNs())
102 }
103 }
104
105 // TestPercentile pins the approximate percentile: it returns the upper bound of
106 // the bucket holding the target sample, 0 when empty, and the last-bucket value
107 // for p100.
108 func TestPercentile(t *testing.T) {
109 empty := NewHistogram("e")
110 if empty.Percentile(50) != 0 {
111 t.Fatalf("empty Percentile = %d, want 0", empty.Percentile(50))
112 }
113 if empty.AvgNs() != 0 {
114 t.Fatalf("empty AvgNs = %d, want 0", empty.AvgNs())
115 }
116
117 h := NewHistogram("p")
118 h.Observe(0)
119 h.Observe(100)
120 // p50 target = 1: the first sample (bucket 0) satisfies it -> upper bound 2.
121 if h.Percentile(50) != 2 {
122 t.Fatalf("p50 = %d, want 2", h.Percentile(50))
123 }
124 // p95 target = 2*95/100 = 1 -> still bucket 0.
125 if h.Percentile(95) != 2 {
126 t.Fatalf("p95 = %d, want 2", h.Percentile(95))
127 }
128 // p99 target = 2*99/100 = 1 -> still bucket 0.
129 if h.Percentile(99) != 2 {
130 t.Fatalf("p99 = %d, want 2", h.Percentile(99))
131 }
132 // p100 target = 2: must walk to bucket 6 -> upper bound 1<<7 = 128.
133 if h.Percentile(100) != 128 {
134 t.Fatalf("p100 = %d, want 128", h.Percentile(100))
135 }
136 }
137
138 // TestReset pins that Reset zeroes Count, SumNs and every bucket, restoring the
139 // empty histogram's percentile/avg answers.
140 func TestReset(t *testing.T) {
141 h := NewHistogram("r")
142 h.Observe(0)
143 h.Observe(64)
144 h.Observe(4096)
145 h.Reset()
146 if h.Count != 0 {
147 t.Fatalf("Count after Reset = %d", h.Count)
148 }
149 if h.SumNs != 0 {
150 t.Fatalf("SumNs after Reset = %d", h.SumNs)
151 }
152 if h.AvgNs() != 0 {
153 t.Fatalf("AvgNs after Reset = %d", h.AvgNs())
154 }
155 if h.Percentile(50) != 0 {
156 t.Fatalf("Percentile after Reset = %d", h.Percentile(50))
157 }
158 for i := 0; i < numBuckets; i++ {
159 if h.Buckets[i] != 0 {
160 t.Fatalf("bucket %d after Reset = %d", i, h.Buckets[i])
161 }
162 }
163 }
164
165 // TestAppendUint64 pins the fmt-free base-10 encoder, including zero (which has
166 // no digits and must not return an empty string).
167 func TestAppendUint64(t *testing.T) {
168 if string(appendUint64(nil, 0)) != "0" {
169 t.Fatalf("appendUint64(0) = %q", string(appendUint64(nil, 0)))
170 }
171 if string(appendUint64(nil, 7)) != "7" {
172 t.Fatalf("appendUint64(7) = %q", string(appendUint64(nil, 7)))
173 }
174 if string(appendUint64(nil, 12345)) != "12345" {
175 t.Fatalf("appendUint64(12345) = %q", string(appendUint64(nil, 12345)))
176 }
177 if string(appendUint64([]byte("x"), 10)) != "x10" {
178 t.Fatalf("appendUint64 append = %q", string(appendUint64([]byte("x"), 10)))
179 }
180 }
181
182 // TestAppendJSONShape pins the exact JSON document for an empty histogram, then
183 // checks that an observation shows up in the count and bucket array.
184 func TestAppendJSONShape(t *testing.T) {
185 h := NewHistogram("x")
186 want := []byte("\"x\":{\"count\":0,\"sum_ns\":0,\"avg_ns\":0,\"p50_ns\":0,\"p95_ns\":0,\"p99_ns\":0,\"buckets\":[")
187 for i := 0; i < numBuckets; i++ {
188 if i > 0 {
189 want = want | ","
190 }
191 want = want | "0"
192 }
193 want = want | "]}"
194 got := h.AppendJSON(nil)
195 if !bytes.Equal(got, want) {
196 t.Fatalf("AppendJSON = %s, want %s", string(got), string(want))
197 }
198
199 h.Observe(1)
200 got2 := h.AppendJSON(nil)
201 if !mxHas(got2, []byte("\"count\":1")) {
202 t.Fatalf("AppendJSON after Observe missing count: %s", string(got2))
203 }
204 if !mxHas(got2, []byte("\"buckets\":[1,")) {
205 t.Fatalf("AppendJSON after Observe missing bucket 0: %s", string(got2))
206 }
207 }
208
209 // TestNowAndSince pins the monotonic timestamp helpers: Now is a positive Unix
210 // nanosecond value and Since never returns a negative elapsed time.
211 func TestNowAndSince(t *testing.T) {
212 if Now() <= 0 {
213 t.Fatal("Now() must be positive")
214 }
215 if Since(Now()) < 0 {
216 t.Fatal("Since(Now()) must not be negative")
217 }
218 start := Now()
219 if Since(start) < 0 {
220 t.Fatal("Since(start) must not be negative")
221 }
222 }
223
224 // TestSnapshotAllAndResetAll pins that the registry covers every declared
225 // histogram by name, and that resetting the registry clears one that was
226 // observed.
227 func TestSnapshotAllAndResetAll(t *testing.T) {
228 ResetAll()
229 names := []string{
230 "ingest_pipeline_ns", "sig_verify_ns", "acl_allowwrite_ns",
231 "wal_append_ns", "wal_fsync_ns", "accept_handle_ns",
232 "accept_wait_ns", "handle_event_ns", "envelope_parse_ns",
233 "send_ws_ns", "broadcast_ns", "ws_frame_ns", "on_poll_ns",
234 }
235 if len(allHistograms) != len(names) {
236 t.Fatalf("registry size = %d, want %d", int32(len(allHistograms)), int32(len(names)))
237 }
238 buf := SnapshotAll(nil)
239 if !bytes.HasPrefix(buf, []byte("{")) || !bytes.HasSuffix(buf, []byte("}")) {
240 t.Fatalf("SnapshotAll is not a JSON object: %s", string(buf))
241 }
242 for _, n := range names {
243 marker := "\"" | n | "\":{\"count\":"
244 if !mxHas(buf, []byte(marker)) {
245 t.Fatalf("SnapshotAll missing %s", n)
246 }
247 }
248
249 // The globals are writable through their pointers; observing one and then
250 // resetting the registry must bring it back to empty.
251 SigVerifyNs.Observe(1234)
252 if SigVerifyNs.Count != 1 || SigVerifyNs.SumNs != 1234 {
253 t.Fatalf("global observe: count=%d sum=%d", SigVerifyNs.Count, SigVerifyNs.SumNs)
254 }
255 ResetAll()
256 if SigVerifyNs.Count != 0 || SigVerifyNs.SumNs != 0 {
257 t.Fatalf("ResetAll left sig_verify_ns: count=%d sum=%d", SigVerifyNs.Count, SigVerifyNs.SumNs)
258 }
259 }
260