Files
frp/pkg/metrics/text/runtime_test.go
T
fatedier 5c0a125c16 deps: bump prometheus/client_golang to v1.25.0 (#5599)
Move client_golang to v1.25.0 with its required module updates
(client_model v0.6.3, common v0.72.0, procfs v0.22.0, xxhash v2.3.0,
protobuf v1.36.12, munnerz/goautoneg). go.mod keeps go 1.26.0 and has
no toolchain line.

Sync the frpc text metrics clone with the new default Go collector:
add go_gc_gogc_percent, go_gc_gomemlimit_bytes and
go_sched_gomaxprocs_threads, update the go_memstats help texts, and
remove go_memstats_lookups_total. frpc still exposes go_* metrics only.
2026-11-04 14:43:00 +08:00

193 lines
6.8 KiB
Go

// Copyright 2026 The frp Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package text
import (
"math"
"runtime"
"runtime/metrics"
"strings"
"sync"
"testing"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
dto "github.com/prometheus/client_model/go"
"github.com/stretchr/testify/require"
)
// TestMemStatsMapping pins the runtime/metrics to MemStats mapping field by
// field. The sample values are made up so that every combination shows up: tiny
// allocations are added to both Mallocs and Frees, and the combined fields
// (HeapInuse, HeapIdle, HeapSys, StackSys, MSpanSys, MCacheSys) add their parts.
func TestMemStatsMapping(t *testing.T) {
samples := map[string]uint64{
rmGCHeapTinyAllocsObjects: 3,
rmGCHeapAllocsObjects: 100,
rmGCHeapFreesObjects: 40,
rmGCHeapAllocsBytes: 1 << 20,
rmGCHeapObjects: 900,
rmGCHeapGoalBytes: 4 << 20,
rmMemoryClassesTotalBytes: 50 << 20,
rmMemoryClassesHeapObjectsBytes: 2 << 20,
rmMemoryClassesHeapUnusedBytes: 3 << 20,
rmMemoryClassesHeapReleasedBytes: 5 << 20,
rmMemoryClassesHeapFreeBytes: 7 << 20,
rmMemoryClassesHeapStacksBytes: 1 << 20,
rmMemoryClassesOSStacksBytes: 2 << 20,
rmMemoryClassesMetadataMSpanInuseBytes: 1 << 19,
rmMemoryClassesMetadataMSpanFreeBytes: 1 << 18,
rmMemoryClassesMetadataMCacheInuseBytes: 1 << 17,
rmMemoryClassesMetadataMCacheFreeBytes: 1 << 16,
rmMemoryClassesProfilingBucketsBytes: 1 << 15,
rmMemoryClassesMetadataOtherBytes: 1 << 14,
rmMemoryClassesOtherBytes: 1 << 13,
}
lookup := func(name string) uint64 { return samples[name] }
var ms runtime.MemStats
memStatsFromRM(&ms, lookup)
// Expectations are keyed by family name, so this also checks that each family
// reads the field the mapping fills.
want := map[string]float64{
"go_memstats_alloc_bytes": 2 << 20,
"go_memstats_alloc_bytes_total": 1 << 20,
"go_memstats_buck_hash_sys_bytes": 1 << 15,
"go_memstats_frees_total": 43, // 40 frees + 3 tiny allocs
"go_memstats_gc_sys_bytes": 1 << 14,
"go_memstats_heap_alloc_bytes": 2 << 20,
"go_memstats_heap_idle_bytes": 12 << 20, // released + free
"go_memstats_heap_inuse_bytes": 5 << 20, // objects + unused
"go_memstats_heap_objects": 900,
"go_memstats_heap_released_bytes": 5 << 20,
"go_memstats_heap_sys_bytes": 17 << 20, // inuse + idle
"go_memstats_mallocs_total": 103, // 100 mallocs + 3 tiny allocs
"go_memstats_mcache_inuse_bytes": 1 << 17,
"go_memstats_mcache_sys_bytes": 3 << 16, // inuse + free
"go_memstats_mspan_inuse_bytes": 1 << 19,
"go_memstats_mspan_sys_bytes": 3 << 18, // inuse + free
"go_memstats_next_gc_bytes": 4 << 20,
"go_memstats_other_sys_bytes": 1 << 13,
"go_memstats_stack_inuse_bytes": 1 << 20,
"go_memstats_stack_sys_bytes": 3 << 20, // inuse + os-stacks
"go_memstats_sys_bytes": 50 << 20,
}
require.Len(t, want, len(memstatsFamilies), "every family needs an expectation")
for _, m := range memstatsFamilies {
expected, ok := want[m.name]
require.True(t, ok, "%s has no expectation", m.name)
require.Equal(t, expected, m.get(&ms), "%s", m.name)
}
}
// TestMemorySamplesMissingName checks that a runtime which does not expose a
// name reads it as zero instead of failing, the way client_golang behaves.
func TestMemorySamplesMissingName(t *testing.T) {
s := &memorySamples{
samples: []metrics.Sample{{Name: rmGCHeapObjects}},
index: map[string]int{rmGCHeapObjects: 0},
}
require.Equal(t, uint64(0), s.value(rmMemoryClassesTotalBytes))
}
// TestMemStatsSameOrderAsClientGolang compares what frpc reports with what
// client_golang reads from the same process at about the same time. The check is
// deliberately loose: the two collectors sample the runtime one after the other,
// so counters move by whatever the test itself allocates in between, and an exact
// comparison would flake.
func TestMemStatsSameOrderAsClientGolang(t *testing.T) {
reg := prometheus.NewRegistry()
reg.MustRegister(collectors.NewGoCollector())
mfs, err := reg.Gather()
require.NoError(t, err)
ours := parseFamilies(t, gather(t))
checked := 0
for _, mf := range mfs {
name := mf.GetName()
if !strings.HasPrefix(name, "go_memstats_") {
continue
}
if name == "go_memstats_last_gc_time_seconds" {
// A collection that starts between the two reads jumps this value from
// zero to the current time; that is not a difference in behavior.
continue
}
want, ok := ours[name]
require.True(t, ok, "%s is missing", name)
reference := metricValue(t, mf.GetMetric()[0])
got := metricValue(t, want.GetMetric()[0])
require.True(t, sameOrder(reference, got),
"%s differs too much: ours %v, client_golang %v", name, got, reference)
checked++
}
require.Greater(t, checked, 15, "comparison covered too few families")
}
func metricValue(t *testing.T, m *dto.Metric) float64 {
t.Helper()
if g := m.GetGauge(); g != nil {
return g.GetValue()
}
if c := m.GetCounter(); c != nil {
return c.GetValue()
}
t.Fatalf("unexpected metric type for %s", m.String())
return 0
}
// sameOrder reports whether two readings of the same quantity are close enough
// that they cannot be different quantities: equal, within 64 KiB (drift while
// the test allocates), or within a factor of ten.
func sameOrder(a, b float64) bool {
if a == b {
return true
}
if math.Abs(a-b) <= 1<<16 {
return true
}
if a <= 0 || b <= 0 {
return false
}
ratio := a / b
return ratio > 0.1 && ratio < 10
}
// TestConcurrentScrapes covers the shared runtime/metrics sample buffer: scrapes
// run on the listener's goroutines and must not race. Run with -race to check it.
func TestConcurrentScrapes(t *testing.T) {
reg := NewRegistry()
reg.RegisterRuntime()
var wg sync.WaitGroup
for range 8 {
wg.Go(func() {
for range 20 {
var b strings.Builder
if err := reg.WritePrometheus(&b); err != nil {
t.Errorf("scrape failed: %v", err)
return
}
if !strings.Contains(b.String(), "go_memstats_heap_objects") {
t.Error("scrape is missing go_memstats_heap_objects")
return
}
}
})
}
wg.Wait()
}