tsweb: remove allocs introduced by earlier change

This removes the ~9 allocs added by #5869, while still keeping struct
fields sorted (the previous commit's tests still pass). And add a test
to lock it in that this shouldn't allocate.

Updates #5778

Change-Id: I4c12b9e2a1334adc1ea5aba1777681cb9fc18fbf
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2022-10-10 09:56:26 -07:00 committed by Brad Fitzpatrick
parent 529e893f70
commit 718914b697
2 changed files with 76 additions and 28 deletions

View File

@ -43,9 +43,11 @@ func init() {
expvar.Publish("gauge_goroutines", expvar.Func(func() any { return runtime.NumGoroutine() })) expvar.Publish("gauge_goroutines", expvar.Func(func() any { return runtime.NumGoroutine() }))
} }
const gaugePrefix = "gauge_" const (
const counterPrefix = "counter_" gaugePrefix = "gauge_"
const labelMapPrefix = "labelmap_" counterPrefix = "counter_"
labelMapPrefix = "labelmap_"
)
// prefixesToTrim contains key prefixes to remove when exporting and sorting metrics. // prefixesToTrim contains key prefixes to remove when exporting and sorting metrics.
var prefixesToTrim = []string{gaugePrefix, counterPrefix, labelMapPrefix} var prefixesToTrim = []string{gaugePrefix, counterPrefix, labelMapPrefix}
@ -642,13 +644,25 @@ func writeMemstats(w io.Writer, ms *runtime.MemStats) {
c("num_gc", uint64(ms.NumGC), "number of completed GC cycles") c("num_gc", uint64(ms.NumGC), "number of completed GC cycles")
} }
// foreachExportedStructField iterates over the fields in sorted order of // sortedStructField is metadata about a struct field used both for sorting once
// their name, after removing metric prefixes. This is not necessarily the // (by structTypeSortedFields) and at serving time (by
// order they were declared in the struct // foreachExportedStructField).
func foreachExportedStructField(rv reflect.Value, f func(fieldOrJSONName, metricType string, rv reflect.Value)) { type sortedStructField struct {
t := rv.Type() Index int // index of struct field in struct
nameToIndex := map[string]int{} Name string // struct field name, or "json" name
sortedFields := make([]string, 0, t.NumField()) SortName string // Name with "foo_" type prefixes removed
MetricType string // the "metrictype" struct tag
StructFieldType *reflect.StructField
}
var structSortedFieldsCache sync.Map // reflect.Type => []sortedStructField
// structTypeSortedFields returns the sorted fields of t, caching as needed.
func structTypeSortedFields(t reflect.Type) []sortedStructField {
if v, ok := structSortedFieldsCache.Load(t); ok {
return v.([]sortedStructField)
}
fields := make([]sortedStructField, 0, t.NumField())
for i, n := 0, t.NumField(); i < n; i++ { for i, n := 0, t.NumField(); i < n; i++ {
sf := t.Field(i) sf := t.Field(i)
name := sf.Name name := sf.Name
@ -662,28 +676,45 @@ func foreachExportedStructField(rv reflect.Value, f func(fieldOrJSONName, metric
name = v name = v
} }
} }
nameToIndex[name] = i fields = append(fields, sortedStructField{
sortedFields = append(sortedFields, name) Index: i,
Name: name,
SortName: removeTypePrefixes(name),
MetricType: sf.Tag.Get("metrictype"),
StructFieldType: &sf,
})
} }
sort.Slice(sortedFields, func(i, j int) bool { sort.Slice(fields, func(i, j int) bool {
left := sortedFields[i] return fields[i].SortName < fields[j].SortName
right := sortedFields[j]
for _, prefix := range prefixesToTrim {
left = strings.TrimPrefix(left, prefix)
right = strings.TrimPrefix(right, prefix)
}
return left < right
}) })
for _, name := range sortedFields { structSortedFieldsCache.Store(t, fields)
i := nameToIndex[name] return fields
sf := t.Field(i) }
metricType := sf.Tag.Get("metrictype")
if metricType != "" || sf.Type.Kind() == reflect.Struct { // removeTypePrefixes returns s with the first "foo_" prefix in prefixesToTrim
f(name, metricType, rv.Field(i)) // removed.
func removeTypePrefixes(s string) string {
for _, prefix := range prefixesToTrim {
if trimmed := strings.TrimPrefix(s, prefix); trimmed != s {
return trimmed
}
}
return s
}
// foreachExportedStructField iterates over the fields in sorted order of
// their name, after removing metric prefixes. This is not necessarily the
// order they were declared in the struct
func foreachExportedStructField(rv reflect.Value, f func(fieldOrJSONName, metricType string, rv reflect.Value)) {
t := rv.Type()
for _, ssf := range structTypeSortedFields(t) {
sf := ssf.StructFieldType
if ssf.MetricType != "" || sf.Type.Kind() == reflect.Struct {
f(ssf.Name, ssf.MetricType, rv.Field(ssf.Index))
} else if sf.Type.Kind() == reflect.Ptr && sf.Type.Elem().Kind() == reflect.Struct { } else if sf.Type.Kind() == reflect.Ptr && sf.Type.Elem().Kind() == reflect.Struct {
fv := rv.Field(i) fv := rv.Field(ssf.Index)
if !fv.IsNil() { if !fv.IsNil() {
f(name, metricType, fv.Elem()) f(ssf.Name, ssf.MetricType, fv.Elem())
} }
} }
} }

View File

@ -14,6 +14,7 @@
"net" "net"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"reflect"
"strings" "strings"
"testing" "testing"
"time" "time"
@ -726,3 +727,19 @@ func TestPort80Handler(t *testing.T) {
}) })
} }
} }
func TestSortedStructAllocs(t *testing.T) {
f := reflect.ValueOf(struct {
Foo int
Bar int
Baz int
}{})
n := testing.AllocsPerRun(1000, func() {
foreachExportedStructField(f, func(fieldOrJSONName, metricType string, rv reflect.Value) {
// Nothing.
})
})
if n != 0 {
t.Errorf("allocs = %v; want 0", n)
}
}