master
go 93 lines 3.3 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package prometheus
4
5 import (
6 "fmt"
7 "net/http"
8 "net/http/httptest"
9 "strings"
10 "testing"
11
12 "github.com/netdata/netdata/go/plugins/logger"
13 "github.com/netdata/netdata/go/plugins/pkg/metrix"
14 prompkg "github.com/netdata/netdata/go/plugins/pkg/prometheus"
15 "github.com/netdata/netdata/go/plugins/pkg/web"
16 )
17
18 // BenchmarkMetricFamilyWriter measures the steady-state cost of writing a high-cardinality scrape
19 // (seriesPerType series of each of the four metric types) to metrix each cycle. The scrape is parsed
20 // once; the loop exercises only writeMetricFamilies so the per-series instrument-resolution cost is
21 // isolated from parsing/HTTP.
22 //
23 // Indicative results on a developer laptop (macOS, 14 logical CPUs; `-benchmem -count=3`),
24 // 2000 series/cycle — relative figures, not an absolute or CI baseline:
25 //
26 // per-series resolve, no cache: ~1.60 ms/op 3.46 MB/op 38088 allocs/op
27 // bounded per-series handle cache: ~0.96 ms/op 1.92 MB/op 18608 allocs/op
28 //
29 // The writer caches the per-series instrument handle and evicts it after seriesCacheRetentionCycles
30 // unobserved cycles. A metrix vec is deliberately NOT used: its vecCache is unbounded for the vec's
31 // lifetime and is not pruned by store retention (pkg/metrix/vec.go), so it would leak under
32 // label-value churn; the bounded cache keeps the speedup without that risk.
33 func BenchmarkMetricFamilyWriter(b *testing.B) {
34 const seriesPerType = 500
35
36 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
37 _, _ = w.Write([]byte(buildBenchExposition(seriesPerType)))
38 }))
39 defer srv.Close()
40
41 mfs, err := prompkg.New(srv.Client(), web.RequestConfig{URL: srv.URL}).Scrape()
42 if err != nil {
43 b.Fatal(err)
44 }
45
46 store := metrix.NewCollectorStore()
47 w := newMetricFamilyWriter(store, metricFamilyWriterPolicy{}, logger.New())
48 managed, ok := metrix.AsCycleManagedStore(store)
49 if !ok {
50 b.Fatal("store is not cycle-managed")
51 }
52 cc := managed.CycleController()
53
54 b.ReportAllocs()
55 b.ResetTimer()
56 for i := 0; i < b.N; i++ {
57 cc.BeginCycle()
58 w.writeMetricFamilies(mfs)
59 if err := cc.CommitCycleSuccess(); err != nil {
60 b.Fatal(err)
61 }
62 }
63 }
64
65 func buildBenchExposition(n int) string {
66 var b strings.Builder
67
68 b.WriteString("# TYPE bench_gauge_bytes gauge\n")
69 for i := range n {
70 fmt.Fprintf(&b, "bench_gauge_bytes{id=\"%d\",az=\"a\"} %d\n", i, i)
71 }
72 b.WriteString("# TYPE bench_ops_total counter\n")
73 for i := range n {
74 fmt.Fprintf(&b, "bench_ops_total{id=\"%d\",az=\"a\"} %d\n", i, i)
75 }
76 b.WriteString("# TYPE bench_latency_seconds summary\n")
77 for i := range n {
78 fmt.Fprintf(&b, "bench_latency_seconds{id=\"%d\",quantile=\"0.5\"} 0.1\n", i)
79 fmt.Fprintf(&b, "bench_latency_seconds{id=\"%d\",quantile=\"0.9\"} 0.2\n", i)
80 fmt.Fprintf(&b, "bench_latency_seconds_sum{id=\"%d\"} 1.0\n", i)
81 fmt.Fprintf(&b, "bench_latency_seconds_count{id=\"%d\"} 10\n", i)
82 }
83 b.WriteString("# TYPE bench_dur_seconds histogram\n")
84 for i := range n {
85 fmt.Fprintf(&b, "bench_dur_seconds_bucket{id=\"%d\",le=\"0.1\"} 1\n", i)
86 fmt.Fprintf(&b, "bench_dur_seconds_bucket{id=\"%d\",le=\"0.5\"} 2\n", i)
87 fmt.Fprintf(&b, "bench_dur_seconds_bucket{id=\"%d\",le=\"+Inf\"} 3\n", i)
88 fmt.Fprintf(&b, "bench_dur_seconds_sum{id=\"%d\"} 0.5\n", i)
89 fmt.Fprintf(&b, "bench_dur_seconds_count{id=\"%d\"} 3\n", i)
90 }
91
92 return b.String()
93 }