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