mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-25 19:15:34 +00:00
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:
parent
529e893f70
commit
718914b697
@ -43,9 +43,11 @@ func init() {
|
||||
expvar.Publish("gauge_goroutines", expvar.Func(func() any { return runtime.NumGoroutine() }))
|
||||
}
|
||||
|
||||
const gaugePrefix = "gauge_"
|
||||
const counterPrefix = "counter_"
|
||||
const labelMapPrefix = "labelmap_"
|
||||
const (
|
||||
gaugePrefix = "gauge_"
|
||||
counterPrefix = "counter_"
|
||||
labelMapPrefix = "labelmap_"
|
||||
)
|
||||
|
||||
// prefixesToTrim contains key prefixes to remove when exporting and sorting metrics.
|
||||
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")
|
||||
}
|
||||
|
||||
// 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()
|
||||
nameToIndex := map[string]int{}
|
||||
sortedFields := make([]string, 0, t.NumField())
|
||||
// sortedStructField is metadata about a struct field used both for sorting once
|
||||
// (by structTypeSortedFields) and at serving time (by
|
||||
// foreachExportedStructField).
|
||||
type sortedStructField struct {
|
||||
Index int // index of struct field in struct
|
||||
Name string // struct field name, or "json" name
|
||||
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++ {
|
||||
sf := t.Field(i)
|
||||
name := sf.Name
|
||||
@ -662,28 +676,45 @@ func foreachExportedStructField(rv reflect.Value, f func(fieldOrJSONName, metric
|
||||
name = v
|
||||
}
|
||||
}
|
||||
nameToIndex[name] = i
|
||||
sortedFields = append(sortedFields, name)
|
||||
fields = append(fields, sortedStructField{
|
||||
Index: i,
|
||||
Name: name,
|
||||
SortName: removeTypePrefixes(name),
|
||||
MetricType: sf.Tag.Get("metrictype"),
|
||||
StructFieldType: &sf,
|
||||
})
|
||||
}
|
||||
sort.Slice(sortedFields, func(i, j int) bool {
|
||||
left := sortedFields[i]
|
||||
right := sortedFields[j]
|
||||
for _, prefix := range prefixesToTrim {
|
||||
left = strings.TrimPrefix(left, prefix)
|
||||
right = strings.TrimPrefix(right, prefix)
|
||||
}
|
||||
return left < right
|
||||
sort.Slice(fields, func(i, j int) bool {
|
||||
return fields[i].SortName < fields[j].SortName
|
||||
})
|
||||
for _, name := range sortedFields {
|
||||
i := nameToIndex[name]
|
||||
sf := t.Field(i)
|
||||
metricType := sf.Tag.Get("metrictype")
|
||||
if metricType != "" || sf.Type.Kind() == reflect.Struct {
|
||||
f(name, metricType, rv.Field(i))
|
||||
structSortedFieldsCache.Store(t, fields)
|
||||
return fields
|
||||
}
|
||||
|
||||
// removeTypePrefixes returns s with the first "foo_" prefix in prefixesToTrim
|
||||
// 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 {
|
||||
fv := rv.Field(i)
|
||||
fv := rv.Field(ssf.Index)
|
||||
if !fv.IsNil() {
|
||||
f(name, metricType, fv.Elem())
|
||||
f(ssf.Name, ssf.MetricType, fv.Elem())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"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)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user