mirror of
https://github.com/fatedier/frp.git
synced 2026-11-05 02:21:37 +08:00
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.
193 lines
6.8 KiB
Go
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()
|
|
}
|